Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions .github/workflows/documentation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ jobs:
key: pagefind-binary-v1-${{ runner.os }}-${{ runner.arch }}-${{ env.PAGEFIND_REVISION }}

- uses: actions/setup-node@v7
if: steps.pagefind-cache.outputs.cache-hit != 'true'
with:
node-version: 24
cache: npm

- name: Install Pagefind Rust toolchain
if: steps.pagefind-cache.outputs.cache-hit != 'true'
Expand All @@ -63,7 +63,6 @@ jobs:
bundler-cache: true

- name: Installing packages
if: steps.pagefind-cache.outputs.cache-hit != 'true'
run: sudo apt-get install wget

- name: Build Pagefind fork
Expand All @@ -76,6 +75,29 @@ jobs:
env:
PAGEFIND_BINARY_PATH: ${{ github.workspace }}/.pagefind/target/release/pagefind
run: bundle exec bake utopia:project:static --force no

- name: Install browser test dependencies
run: |
npm ci
npx playwright install --with-deps chromium

- name: Build browser fixture
timeout-minutes: 5
env:
PAGEFIND_BINARY_PATH: ${{ github.workspace }}/.pagefind/target/release/pagefind
run: bundle exec ruby fixtures/utopia/project/build_site.rb

- name: Test generated documentation in Chromium
timeout-minutes: 5
run: npm run test:browser

- name: Upload browser failure diagnostics
if: failure()
uses: actions/upload-artifact@v7
with:
name: browser-test-results
path: test-results
if-no-files-found: ignore

- name: Upload documentation artifact
uses: actions/upload-pages-artifact@v5
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@

/node_modules
/.github/workflows/test-external.yaml

/test/browser/.site
/test-results
/playwright-report
2 changes: 0 additions & 2 deletions bake/utopia/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,6 @@ def description(root: context.root)
child = document.first_child

if child&.type == :header
title = child.first_child.string_content

# First sentence
if introduction = child.next
$stdout.puts introduction.to_plaintext[/.*?\./]
Expand Down
2 changes: 1 addition & 1 deletion bake/utopia/project/agent/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def update
files << {
"path" => guide.name + ".md",
"title" => guide.title,
"description" => guide.description.to_markdown.chomp,
"description" => guide.description&.to_markdown&.chomp || "",
}
end

Expand Down
9 changes: 9 additions & 0 deletions config/covered.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

# Include exported tasks and controllers even when no test loads them.
def include_patterns
super + ["bake/**/*.rb", "pages/**/*.rb"]
end
14 changes: 14 additions & 0 deletions fixtures/utopia/project/build_site.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

require "bake/context"
require "fileutils"

root = File.expand_path("../../../test/utopia/project/.fixtures/site", __dir__)
output = File.expand_path("../../../test/browser/.site", __dir__)

Dir.chdir(root) do
Bake::Context.load(root)["utopia:project:static"].call(output_path: output)
end
50 changes: 50 additions & 0 deletions fixtures/utopia/project/site.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

require "tmpdir"
require "fileutils"
require "utopia/project"
require_relative "../../../template/config/environment"
require "sus/fixtures/protocol/http/middleware_context"

module Utopia
module Project
module SiteContext
include Sus::Fixtures::Protocol::HTTP::MiddlewareContext

SITE = File.expand_path("../../../test/utopia/project/.fixtures/site", __dir__)

def around(&block)
previous = Thread.current.thread_variable_get(Base.name)
Dir.mktmpdir("utopia-project-test") do |root|
@root = root
FileUtils.cp_r("#{SITE}/.", root)
Base.instance = base
super(&block)
end
ensure
Base.instance = previous
end

def base
@base ||= Base.new(@root).tap do |base|
base.update(Dir.glob("#{@root}/lib/**/*.rb"))
end
end

def middleware
@middleware ||= Utopia::Application.build do |builder|
Project.call(builder, @root)
end
end

def write(path, content)
path = File.join(@root, path)
FileUtils.mkdir_p(File.dirname(path))
File.write(path, content)
end
end
end
end
4 changes: 2 additions & 2 deletions lib/utopia/project/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,12 @@ def lookup(path)
# @parameter definition [Decode::Definition] The definition to load documentation for.
# @returns [Document | Nil] The supplemental document, if it exists.
def document_for(definition)
document_path = File.join("lib", definition.lexical_path.map{|_| _.to_s.downcase}) + ".md"
document_path = File.join(@root, "lib", definition.lexical_path.map{|_| _.to_s.downcase}) + ".md"

if File.exist?(document_path)
document = self.document(File.read(document_path), definition)

if document.first_child.type == :header
if document.first_child&.type == :header
document.first_child.delete
end

Expand Down
6 changes: 3 additions & 3 deletions lib/utopia/project/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ def root
end

# Extract the leading heading as the document title.
# @returns [String | Nil] The title, if the document starts with a heading.
# @returns [String | Nil] The title, if the document starts with a non-empty heading.
def title
child = self.root.first_child

if child && child.type == :header
return child.first_child.to_plaintext
return child.first_child&.to_plaintext
end
end

Expand All @@ -61,7 +61,7 @@ def replace_section(name, children: false)
header = child

# We found the matched header:
if header.first_child.to_plaintext.include?(name)
if header.first_child&.to_plaintext&.include?(name)
# Now subsequent children:
current = header.next

Expand Down
27 changes: 3 additions & 24 deletions lib/utopia/project/guide.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,32 +44,11 @@ def order
metadata[:order]
end

# Compare guides by explicit order and then by name.
# Compare guides by order (defaulting to zero) and then by name.
# @parameter other [Guide] The other guide to compare.
# @returns [Integer] The comparison result.
def <=> other
if order = self.order
if other_order = other.order
if order < other_order
return -1
elsif order > other_order
return 1
end
else
# If we have order, but the other doesn't, we come first:
return -1
end
end

if name = self.name
if other_name = other.name
return name <=> other_name
else
return -1
end
end

return 0
[self.order || 0, self.name] <=> [other.order || 0, other.name]
end

README = "readme.md"
Expand All @@ -93,7 +72,7 @@ def document
child = document.first_child

if child&.type == :header
@title = child.first_child.string_content
@title = child.first_child&.string_content

@description = child.next
child.delete
Expand Down
48 changes: 48 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,11 @@
}
}
}
},
"devDependencies": {
"@playwright/test": "1.63.0"
},
"scripts": {
"test:browser": "playwright test"
}
}
2 changes: 2 additions & 0 deletions pages/guides/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@
guide.name == name
end

respond! Utopia::Response[404] unless @guide

path.components = ["show"]
end
6 changes: 3 additions & 3 deletions pages/index.xnode
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
if document = self[:document]
child = document.first_child

if child.type == :header
if child&.type == :header
header = child
child.delete
title = header.first_child

case title.type
case title&.type
when :text
?><content:heading>#{title.string_content}</content:heading><?r
when :image
Expand All @@ -35,4 +35,4 @@
<?r
end
?>
</content:page>
</content:page>
2 changes: 1 addition & 1 deletion pages/reference/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
@node, @symbol = @base.lookup(@lexical_path)

unless @symbol
fail! :not_found
respond! Utopia::Response[404]
end

path.components = ["show"]
Expand Down
24 changes: 24 additions & 0 deletions playwright.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const {defineConfig} = require('@playwright/test');

module.exports = defineConfig({
testDir: './test/browser',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: 0,
workers: 2,
use: {
baseURL: 'http://127.0.0.1:9294/project/',
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
launchOptions: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ? {executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH} : {},
},
projects: ['light', 'dark'].flatMap(colorScheme => [390, 1440].map(width => ({
name: `${colorScheme}-${width}`,
use: {browserName: 'chromium', colorScheme, viewport: {width, height: 900}},
}))),
webServer: {
command: 'node test/browser/server.cjs',
url: 'http://127.0.0.1:9294/project/index.html',
reuseExistingServer: false,
},
});
5 changes: 5 additions & 0 deletions releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

- Sort guides by order (defaulting to zero), then name.
- Fix supplemental documentation paths and missing guide/reference responses.
- Handle empty READMEs and guides without descriptions when rendering pages and generating agent context.
- Treat empty Markdown headings as missing titles and preserve the following content when rendering pages or updating documentation.
- Cover all Ruby, task, and rendered template lines, and exercise the generated site in Chromium at mobile and desktop widths in light and dark mode.
- Add padding to documentation table cells and allow tables to scroll whenever they exceed the available width.
- Scale table, inline code, badge, navigation link, and disclosure spacing with the local font size.

Expand Down
Loading
Loading