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
11 changes: 11 additions & 0 deletions .claude/rules/schemas/schema-validation-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
paths:
- "src/resources/schema/**/*"
- "tests/unit/schema-validation/**/*"
---

# Testing schema changes

Test whether a YAML value is accepted or rejected with a table-driven unit test under `tests/unit/schema-validation/`, validating against the real schema (example: `front-matter-social-metadata.test.ts`), not with a smoke-all document per value.
Unit tests load the generated schema resources, so run `dev-call build-artifacts` after a schema edit before running them.
Helpers, which schema to use for front matter vs `_quarto.yml`, and how to prove RED: `llm-docs/testing-patterns.md` § "Schema Validation Tests".
4 changes: 4 additions & 0 deletions .claude/rules/testing/smoke-all-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ Keep the `_quarto: tests:` block assertion-focused, but inline `#` comments labe
**Issue-based:** `tests/docs/smoke-all/YYYY/MM/DD/<issue>.qmd`
**Feature-based:** `tests/docs/smoke-all/<feature>/`

### Schema validation: use a unit test

Checking whether a YAML option is accepted or rejected is a schema question, not a render one: use a unit test under `tests/unit/schema-validation/` (see `.claude/rules/schemas/schema-validation-tests.md`). Keep smoke-all for what the option does in the output.

### Many fixtures: prefer a dynamic smoke test

Every non-`_`-prefixed `.qmd` under `smoke-all/` is globbed and rendered as its own test target. When a test needs many fixture files (e.g. dozens of pages to reproduce a project-level bug), each stored page becomes a redundant per-file render — plus repo bloat — on top of the project render. Generate the project in a temp directory from a TypeScript `tests/smoke/<feature>/` test instead (`testQuartoCmd("render", [tempDir], ...)` with `setup`/`teardown`). See the "Project Rendering Tests" and "Performance Budget" patterns in `llm-docs/testing-patterns.md`.
Expand Down
15 changes: 15 additions & 0 deletions llm-docs/testing-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ key_files:
- tests/quarto-cmd.ts
- tests/verify.ts
- tests/utils.ts
- tests/unit/schema-validation/utils.ts
- src/core/schema/utils.ts
---

# Quarto Test Patterns
Expand Down Expand Up @@ -488,6 +490,19 @@ testQuartoCmd(

Run test **without fix** first to verify it fails, then verify it passes with fix.

## Schema Validation Tests

When the question is whether a YAML value is accepted or rejected (a schema added or changed in `src/resources/schema/`), test it with a unit test under `tests/unit/schema-validation/`, not a smoke-all document per value.
A unit test validates YAML strings directly against the real schema in milliseconds, and a new case is one table row instead of one render.
Example: `tests/unit/schema-validation/front-matter-social-metadata.test.ts`, a table of front matter snippets with the expected outcome.

- Front matter and `_metadata.yml` validate against `getFrontMatterSchema()` (`src/core/lib/yaml-schema/front-matter.ts`), the schema used at render time; `_quarto.yml` validates against `getProjectConfigSchema()` (`src/core/lib/yaml-schema/project-config.ts`).
- Validate with `readAndValidateYamlFromMappedString` and assert on `yamlValidationErrors`; wrap tests in `yamlValidationUnitTest` (`tests/unit/schema-validation/utils.ts`), which initializes the schema state.
- Include valid rows next to invalid ones: they catch a schema that is too strict, such as rejecting an empty value the code handles.
- The schema is loaded from the generated `src/resources/editor/tools/yaml/yaml-intelligence-resources.json`, not from the `.yml` source. Run `dev-call build-artifacts` after editing the schema, otherwise the test runs against the old schema. To prove RED, restore the old generated files (or the old `.yml` and rebuild), not the `.yml` alone.

Use a render test only for what happens after validation, such as the output an option produces.

## Smoke-All Tests (YAML-Based)

Smoke-all tests embed test specifications directly in `.qmd` files using `_quarto.tests` metadata.
Expand Down
1 change: 1 addition & 0 deletions news/changelog-1.11.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ All changes included in 1.11:
- ([#15055](https://github.com/quarto-dev/quarto-cli/issues/15055)): Fix `og:image` and `twitter:image` of website pages using an inline data URI image, such as the ORCID badge of an author, joined to `site-url` instead of the next image of the page or the site `image`. A data URI set as a page, site or listing item `image` is now ignored with a warning for social metadata and listing feeds, as these images are fetched by URL, and listing cards show it as is.
- ([#15057](https://github.com/quarto-dev/quarto-cli/pull/15057)): Fix a numeric top-level `image-width` or `image-height` in page front matter failing the render. These options were never used, so they are removed from the schema; declare the preview image size under `open-graph` instead.
- ([#15083](https://github.com/quarto-dev/quarto-cli/pull/15083)): Fix editors offering no completion for the top-level `image-title` option that about pages read, and correct the description of the top-level `image-lazy-loading` option, which only affects the page's image when it appears in a listing.
- ([#15084](https://github.com/quarto-dev/quarto-cli/pull/15084)): Fix page-level `open-graph` and `twitter-card` options not being validated, which let a misspelled option be silently ignored and a value of the wrong type crash the render. These options are now checked like their `website` counterparts, so a page or `_metadata.yml` with an unknown option under `open-graph` or `twitter-card`, which was ignored before, now fails to render with an error naming the option.

## Lua API

Expand Down
44 changes: 41 additions & 3 deletions src/resources/editor/tools/vs-code.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20710,6 +20710,42 @@ var require_yaml_intelligence_resources = __commonJS({
long: "The title for the main image on the about page. Used when\n`about` does not set its own `image-title`.\n"
}
},
{
name: "open-graph",
schema: {
anyOf: [
"boolean",
null,
{
ref: "open-graph-config"
}
]
},
tags: {
formats: [
"$html-doc"
]
},
description: "Publish open graph metadata for this page. Options set here are merged\nwith the `open-graph` options of the `website` metadata.\n"
},
{
name: "twitter-card",
schema: {
anyOf: [
"boolean",
null,
{
ref: "twitter-card-config"
}
]
},
tags: {
formats: [
"$html-doc"
]
},
description: "Publish twitter card metadata for this page. Options set here are merged\nwith the `twitter-card` options of the `website` metadata.\n"
},
{
name: "image-lazy-loading",
schema: "boolean",
Expand Down Expand Up @@ -24786,6 +24822,8 @@ var require_yaml_intelligence_resources = __commonJS({
short: "The title for the main image on the about page.",
long: "The title for the main image on the about page. Used when\n<code>about</code> does not set its own <code>image-title</code>."
},
"Publish open graph metadata for this page. Options set here are\nmerged with the <code>open-graph</code> options of the\n<code>website</code> metadata.",
"Publish twitter card metadata for this page. Options set here are\nmerged with the <code>twitter-card</code> options of the\n<code>website</code> metadata.",
{
short: "If true, this page\u2019s image in a listing will only load when it comes\ninto view.",
long: 'Enables lazy loading for this page\u2019s image when the page appears as\nan item in a listing. If true, the listing image element will have\n<code>loading="lazy"</code>, and will only load when it comes into\nview.\nIf false, the image will load immediately. This overrides the\nlisting\u2019s <code>image-lazy-loading</code> option for this item.'
Expand Down Expand Up @@ -25726,12 +25764,12 @@ var require_yaml_intelligence_resources = __commonJS({
mermaid: "%%"
},
"handlers/mermaid/schema.yml": {
_internalId: 218225,
_internalId: 219833,
type: "object",
description: "be an object",
properties: {
"mermaid-format": {
_internalId: 218217,
_internalId: 219825,
type: "enum",
enum: [
"png",
Expand All @@ -25747,7 +25785,7 @@ var require_yaml_intelligence_resources = __commonJS({
exhaustiveCompletions: true
},
theme: {
_internalId: 218224,
_internalId: 219832,
type: "anyOf",
anyOf: [
{
Expand Down

Large diffs are not rendered by default.

44 changes: 41 additions & 3 deletions src/resources/editor/tools/yaml/web-worker.js

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

44 changes: 41 additions & 3 deletions src/resources/editor/tools/yaml/yaml-intelligence-resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -13678,6 +13678,42 @@
"long": "The title for the main image on the about page. Used when\n`about` does not set its own `image-title`.\n"
}
},
{
"name": "open-graph",
"schema": {
"anyOf": [
"boolean",
null,
{
"ref": "open-graph-config"
}
]
},
"tags": {
"formats": [
"$html-doc"
]
},
"description": "Publish open graph metadata for this page. Options set here are merged\nwith the `open-graph` options of the `website` metadata.\n"
},
{
"name": "twitter-card",
"schema": {
"anyOf": [
"boolean",
null,
{
"ref": "twitter-card-config"
}
]
},
"tags": {
"formats": [
"$html-doc"
]
},
"description": "Publish twitter card metadata for this page. Options set here are merged\nwith the `twitter-card` options of the `website` metadata.\n"
},
{
"name": "image-lazy-loading",
"schema": "boolean",
Expand Down Expand Up @@ -17754,6 +17790,8 @@
"short": "The title for the main image on the about page.",
"long": "The title for the main image on the about page. Used when\n<code>about</code> does not set its own <code>image-title</code>."
},
"Publish open graph metadata for this page. Options set here are\nmerged with the <code>open-graph</code> options of the\n<code>website</code> metadata.",
"Publish twitter card metadata for this page. Options set here are\nmerged with the <code>twitter-card</code> options of the\n<code>website</code> metadata.",
{
"short": "If true, this page’s image in a listing will only load when it comes\ninto view.",
"long": "Enables lazy loading for this page’s image when the page appears as\nan item in a listing. If true, the listing image element will have\n<code>loading=\"lazy\"</code>, and will only load when it comes into\nview.\nIf false, the image will load immediately. This overrides the\nlisting’s <code>image-lazy-loading</code> option for this item."
Expand Down Expand Up @@ -18694,12 +18732,12 @@
"mermaid": "%%"
},
"handlers/mermaid/schema.yml": {
"_internalId": 218225,
"_internalId": 219833,
"type": "object",
"description": "be an object",
"properties": {
"mermaid-format": {
"_internalId": 218217,
"_internalId": 219825,
"type": "enum",
"enum": [
"png",
Expand All @@ -18715,7 +18753,7 @@
"exhaustiveCompletions": true
},
"theme": {
"_internalId": 218224,
"_internalId": 219832,
"type": "anyOf",
"anyOf": [
{
Expand Down
24 changes: 24 additions & 0 deletions src/resources/schema/document-website.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,30 @@
The title for the main image on the about page. Used when
`about` does not set its own `image-title`.

- name: open-graph
schema:
anyOf:
- boolean
- null
- ref: open-graph-config
tags:
formats: [$html-doc]
description: |
Publish open graph metadata for this page. Options set here are merged
with the `open-graph` options of the `website` metadata.

- name: twitter-card
schema:
anyOf:
- boolean
- null
- ref: twitter-card-config
tags:
formats: [$html-doc]
description: |
Publish twitter card metadata for this page. Options set here are merged
with the `twitter-card` options of the `website` metadata.

- name: image-lazy-loading
schema: boolean
tags:
Expand Down
33 changes: 33 additions & 0 deletions tests/unit/schema-validation/front-matter-social-metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* front-matter-social-metadata.test.ts
*
* Copyright (C) 2026 Posit Software, PBC
*/

import { assertEquals } from "testing/asserts";
import { yamlValidationUnitTest } from "./utils.ts";
import { getFrontMatterSchema } from "../../../src/core/lib/yaml-schema/front-matter.ts";
import { readAndValidateYamlFromMappedString } from "../../../src/core/lib/yaml-schema/validated-yaml.ts";
import { asMappedString } from "../../../src/core/lib/mapped-text.ts";

const cases: [string, string, boolean][] = [
["open-graph unknown key", "open-graph:\n imge-width: 1200\n", false],
["open-graph wrong type", "open-graph:\n description: 42\n", false],
["open-graph valid", "open-graph:\n image-width: 1200\n locale: fr_FR\n", true],
["open-graph boolean", "open-graph: true\n", true],
["open-graph null", "open-graph:\n", true],
["twitter-card unknown key", "twitter-card:\n card-styl: summary\n", false],
["twitter-card valid", "twitter-card:\n card-style: summary\n creator: '@me'\n", true],
["twitter-card null", "twitter-card:\n", true],
];

for (const [name, yaml, valid] of cases) {
yamlValidationUnitTest(`front matter ${name}`, async () => {
const schema = await getFrontMatterSchema();
const { yamlValidationErrors } = await readAndValidateYamlFromMappedString(
asMappedString(`title: Page\n${yaml}`),
schema,
);
assertEquals(yamlValidationErrors.length === 0, valid, yaml);
});
}
Loading