@rm-industries/create-forge 0.4.0-beta.3 → 0.4.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/template/docs/guides/content-and-cms.md +3 -3
- package/dist/template/playwright.config.ts +4 -2
- package/dist/template/scripts/documentation-examples.test.ts +13 -0
- package/dist/template/tests/accessibility.spec.ts +8 -6
- package/dist/template/tests/navigation.spec.ts +9 -7
- package/dist/template/tests/preview.ts +11 -0
- package/dist/template/tests/routes.spec.ts +22 -17
- package/dist/template/tests/theme.spec.ts +6 -4
- package/package.json +1 -1
|
@@ -18,14 +18,14 @@ tags:
|
|
|
18
18
|
draft: false
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
# My first article
|
|
22
|
-
|
|
23
21
|
Write the article body here.
|
|
24
22
|
```
|
|
25
23
|
|
|
26
24
|
The file name becomes the article slug. Use a unique lowercase, hyphenated file
|
|
27
25
|
name. Required fields, dates, defaults, and body content are validated during
|
|
28
|
-
development, type checking, and builds.
|
|
26
|
+
development, type checking, and builds. Do not repeat the title as a top-level
|
|
27
|
+
Markdown heading: the front matter `title` is the article's document title, and
|
|
28
|
+
the article layout renders it as the page heading.
|
|
29
29
|
|
|
30
30
|
Set `draft: true` while writing. Draft articles appear during local development
|
|
31
31
|
but are omitted from production article pages and RSS output. Before publishing:
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { defineConfig, devices } from '@playwright/test';
|
|
2
2
|
|
|
3
|
+
import { previewOrigin, resolvePreviewUrl } from './tests/preview';
|
|
4
|
+
|
|
3
5
|
export default defineConfig({
|
|
4
6
|
testDir: './tests',
|
|
5
7
|
outputDir: 'test-results',
|
|
@@ -8,7 +10,7 @@ export default defineConfig({
|
|
|
8
10
|
retries: process.env.CI ? 2 : 0,
|
|
9
11
|
reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : 'list',
|
|
10
12
|
use: {
|
|
11
|
-
baseURL:
|
|
13
|
+
baseURL: previewOrigin,
|
|
12
14
|
screenshot: 'only-on-failure',
|
|
13
15
|
trace: 'retain-on-failure',
|
|
14
16
|
},
|
|
@@ -24,7 +26,7 @@ export default defineConfig({
|
|
|
24
26
|
...process.env,
|
|
25
27
|
ASTRO_PREVIEW_BACKGROUND: '0',
|
|
26
28
|
},
|
|
27
|
-
url: '
|
|
29
|
+
url: resolvePreviewUrl('/'),
|
|
28
30
|
reuseExistingServer: !process.env.CI,
|
|
29
31
|
},
|
|
30
32
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
describe('documentation examples', () => {
|
|
6
|
+
it('does not duplicate the article title as a Markdown heading', async () => {
|
|
7
|
+
const guide = await readFile(new URL('../docs/guides/content-and-cms.md', import.meta.url), 'utf8');
|
|
8
|
+
const articleExample = /```md\n([\s\S]*?)\n```/u.exec(guide)?.[1];
|
|
9
|
+
|
|
10
|
+
expect(articleExample).toContain('title: My first article');
|
|
11
|
+
expect(articleExample).not.toMatch(/^# /mu);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -2,6 +2,8 @@ import AxeBuilder from '@axe-core/playwright';
|
|
|
2
2
|
import { expect, test } from '@playwright/test';
|
|
3
3
|
import type { Page } from '@playwright/test';
|
|
4
4
|
|
|
5
|
+
import { resolvePreviewPath } from './preview';
|
|
6
|
+
|
|
5
7
|
const publicRoutes = [
|
|
6
8
|
{ name: 'home', path: '/' },
|
|
7
9
|
{ name: 'about', path: '/about/' },
|
|
@@ -22,7 +24,7 @@ for (const colorScheme of ['light', 'dark'] as const) {
|
|
|
22
24
|
for (const route of publicRoutes) {
|
|
23
25
|
test(`${route.name} has no serious or critical violations in ${colorScheme} mode`, async ({ page }) => {
|
|
24
26
|
await page.emulateMedia({ colorScheme });
|
|
25
|
-
await page.goto(route.path);
|
|
27
|
+
await page.goto(resolvePreviewPath(route.path));
|
|
26
28
|
|
|
27
29
|
expect(await analyzePage(page)).toEqual([]);
|
|
28
30
|
});
|
|
@@ -31,7 +33,7 @@ for (const colorScheme of ['light', 'dark'] as const) {
|
|
|
31
33
|
|
|
32
34
|
for (const route of publicRoutes) {
|
|
33
35
|
test(`${route.name} exposes one main landmark and one level-one heading`, async ({ page }) => {
|
|
34
|
-
await page.goto(route.path);
|
|
36
|
+
await page.goto(resolvePreviewPath(route.path));
|
|
35
37
|
|
|
36
38
|
await expect(page.getByRole('banner')).toHaveCount(1);
|
|
37
39
|
await expect(page.getByRole('main')).toHaveCount(1);
|
|
@@ -41,14 +43,14 @@ for (const route of publicRoutes) {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
test('identifies the current primary navigation item', async ({ page }) => {
|
|
44
|
-
await page.goto('/');
|
|
46
|
+
await page.goto(resolvePreviewPath('/'));
|
|
45
47
|
|
|
46
48
|
await expect(page.getByRole('navigation', { name: 'Primary navigation' })).toBeVisible();
|
|
47
49
|
await expect(page.getByRole('link', { name: 'Home' })).toHaveAttribute('aria-current', 'page');
|
|
48
50
|
});
|
|
49
51
|
|
|
50
52
|
test('moves keyboard users directly to the main content', async ({ page }) => {
|
|
51
|
-
await page.goto('/');
|
|
53
|
+
await page.goto(resolvePreviewPath('/'));
|
|
52
54
|
await page.keyboard.press('Tab');
|
|
53
55
|
|
|
54
56
|
const skipLink = page.getByRole('link', { name: 'Skip to content' });
|
|
@@ -62,7 +64,7 @@ test('moves keyboard users directly to the main content', async ({ page }) => {
|
|
|
62
64
|
|
|
63
65
|
test('keeps navigation available without horizontal overflow on small screens', async ({ page }) => {
|
|
64
66
|
await page.setViewportSize({ width: 320, height: 640 });
|
|
65
|
-
await page.goto('/');
|
|
67
|
+
await page.goto(resolvePreviewPath('/'));
|
|
66
68
|
|
|
67
69
|
await page.locator('summary').click();
|
|
68
70
|
await expect(page.getByRole('navigation', { name: 'Mobile navigation' })).toBeVisible();
|
|
@@ -75,7 +77,7 @@ test('keeps navigation available without horizontal overflow on small screens',
|
|
|
75
77
|
|
|
76
78
|
test('honors reduced-motion preferences', async ({ page }) => {
|
|
77
79
|
await page.emulateMedia({ reducedMotion: 'reduce' });
|
|
78
|
-
await page.goto('/');
|
|
80
|
+
await page.goto(resolvePreviewPath('/'));
|
|
79
81
|
|
|
80
82
|
const motionStyles = await page.getByRole('link', { name: 'Browse the articles' }).evaluate((element) => {
|
|
81
83
|
const styles = getComputedStyle(element);
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { expect, test } from '@playwright/test';
|
|
2
2
|
|
|
3
|
+
import { resolvePreviewPath, resolvePreviewUrl } from './preview';
|
|
4
|
+
|
|
3
5
|
test('uses primary navigation to move between pages and identify the current page', async ({ page }) => {
|
|
4
|
-
await page.goto('/');
|
|
6
|
+
await page.goto(resolvePreviewPath('/'));
|
|
5
7
|
|
|
6
8
|
const navigation = page.getByRole('navigation', { name: 'Primary navigation' });
|
|
7
9
|
await expect(navigation.getByRole('link', { name: 'Home' })).toHaveAttribute('aria-current', 'page');
|
|
8
10
|
|
|
9
11
|
await navigation.getByRole('link', { name: 'Articles' }).click();
|
|
10
|
-
await expect(page).toHaveURL('/articles/');
|
|
12
|
+
await expect(page).toHaveURL(resolvePreviewUrl('/articles/'));
|
|
11
13
|
await expect(page.getByRole('heading', { level: 1, name: 'Articles' })).toBeVisible();
|
|
12
14
|
await expect(
|
|
13
15
|
page.getByRole('navigation', { name: 'Primary navigation' }).getByRole('link', { name: 'Articles' }),
|
|
@@ -16,28 +18,28 @@ test('uses primary navigation to move between pages and identify the current pag
|
|
|
16
18
|
|
|
17
19
|
test('opens mobile navigation and follows a configured link', async ({ page }) => {
|
|
18
20
|
await page.setViewportSize({ width: 320, height: 640 });
|
|
19
|
-
await page.goto('/');
|
|
21
|
+
await page.goto(resolvePreviewPath('/'));
|
|
20
22
|
|
|
21
23
|
await page.locator('summary').filter({ hasText: 'Menu' }).click();
|
|
22
24
|
const navigation = page.getByRole('navigation', { name: 'Mobile navigation' });
|
|
23
25
|
await expect(navigation).toBeVisible();
|
|
24
26
|
await navigation.getByRole('link', { name: 'About' }).click();
|
|
25
27
|
|
|
26
|
-
await expect(page).toHaveURL('/about/');
|
|
28
|
+
await expect(page).toHaveURL(resolvePreviewUrl('/about/'));
|
|
27
29
|
await expect(
|
|
28
30
|
page.getByRole('heading', { level: 1, name: 'A useful foundation, not a finished identity.' }),
|
|
29
31
|
).toBeVisible();
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
test('moves from the article listing into an article and through article pagination', async ({ page }) => {
|
|
33
|
-
await page.goto('/articles/');
|
|
35
|
+
await page.goto(resolvePreviewPath('/articles/'));
|
|
34
36
|
|
|
35
37
|
await page.getByRole('link', { name: /Designing a calm starting point/u }).click();
|
|
36
|
-
await expect(page).toHaveURL('/articles/designing-a-calm-starting-point/');
|
|
38
|
+
await expect(page).toHaveURL(resolvePreviewUrl('/articles/designing-a-calm-starting-point/'));
|
|
37
39
|
await expect(page.getByRole('heading', { level: 1, name: 'Designing a calm starting point' })).toBeVisible();
|
|
38
40
|
|
|
39
41
|
const articleNavigation = page.getByRole('navigation', { name: 'Article navigation' });
|
|
40
42
|
await articleNavigation.getByRole('link', { name: /Content that travels well/u }).click();
|
|
41
|
-
await expect(page).toHaveURL('/articles/content-that-travels-well/');
|
|
43
|
+
await expect(page).toHaveURL(resolvePreviewUrl('/articles/content-that-travels-well/'));
|
|
42
44
|
await expect(page.getByRole('heading', { level: 1, name: 'Content that travels well' })).toBeVisible();
|
|
43
45
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getDeploymentConfig } from '../src/config/deployment';
|
|
2
|
+
import { site } from '../src/config/site';
|
|
3
|
+
import { resolveSiteHref } from '../src/lib/paths';
|
|
4
|
+
|
|
5
|
+
export const previewOrigin = 'http://127.0.0.1:4321';
|
|
6
|
+
|
|
7
|
+
const deployment = getDeploymentConfig(site.url);
|
|
8
|
+
|
|
9
|
+
export const resolvePreviewPath = (path: string): string => resolveSiteHref(path, deployment.base ?? '/');
|
|
10
|
+
|
|
11
|
+
export const resolvePreviewUrl = (path: string): string => new URL(resolvePreviewPath(path), previewOrigin).href;
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import { expect, test } from '@playwright/test';
|
|
2
2
|
|
|
3
3
|
import { site } from '../src/config/site';
|
|
4
|
+
import { resolvePreviewPath, resolvePreviewUrl } from './preview';
|
|
4
5
|
|
|
5
6
|
const routes = ['/', '/about/', '/articles/', '/articles/designing-a-calm-starting-point/', '/admin/', '/404/'];
|
|
6
7
|
|
|
7
8
|
test('serves every baseline page with configured canonical and social metadata', async ({ page }) => {
|
|
8
9
|
for (const route of routes) {
|
|
9
|
-
await page.goto(route);
|
|
10
|
+
await page.goto(resolvePreviewPath(route));
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
const canonicalUrl = new URL(resolvePreviewPath(route), site.url).href;
|
|
13
|
+
|
|
14
|
+
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', canonicalUrl);
|
|
12
15
|
await expect(page.locator('meta[property="og:site_name"]')).toHaveAttribute('content', site.name);
|
|
13
|
-
await expect(page.locator('meta[property="og:url"]')).toHaveAttribute('content',
|
|
16
|
+
await expect(page.locator('meta[property="og:url"]')).toHaveAttribute('content', canonicalUrl);
|
|
14
17
|
await expect(page.locator('meta[name="twitter:card"]')).toHaveAttribute('content', 'summary_large_image');
|
|
15
18
|
}
|
|
16
19
|
});
|
|
17
20
|
|
|
18
21
|
test('serves the content manager without allowing search indexing', async ({ page }) => {
|
|
19
|
-
await page.goto('/admin/');
|
|
22
|
+
await page.goto(resolvePreviewPath('/admin/'));
|
|
20
23
|
|
|
21
24
|
await expect(page.locator('meta[name="robots"][content="noindex, nofollow"]')).toHaveCount(1);
|
|
22
25
|
await expect(page).toHaveTitle(/Content Manager/u);
|
|
@@ -24,19 +27,21 @@ test('serves the content manager without allowing search indexing', async ({ pag
|
|
|
24
27
|
});
|
|
25
28
|
|
|
26
29
|
test('renders generic article tags on listings and detail pages', async ({ page }) => {
|
|
27
|
-
await page.goto('/articles/');
|
|
30
|
+
await page.goto(resolvePreviewPath('/articles/'));
|
|
28
31
|
await expect(
|
|
29
|
-
page
|
|
32
|
+
page
|
|
33
|
+
.locator(`a[href="${resolvePreviewPath('/articles/designing-a-calm-starting-point/')}"]`)
|
|
34
|
+
.getByLabel('Article tags'),
|
|
30
35
|
).toContainText('Design');
|
|
31
36
|
|
|
32
|
-
await page.goto('/articles/designing-a-calm-starting-point/');
|
|
37
|
+
await page.goto(resolvePreviewPath('/articles/designing-a-calm-starting-point/'));
|
|
33
38
|
await expect(page.getByLabel('Article tags')).toContainText('Defaults');
|
|
34
39
|
});
|
|
35
40
|
|
|
36
41
|
test('resolves every internal page link', async ({ page, request }) => {
|
|
37
|
-
await page.goto('/');
|
|
42
|
+
await page.goto(resolvePreviewPath('/'));
|
|
38
43
|
|
|
39
|
-
const pending = ['/'];
|
|
44
|
+
const pending = [resolvePreviewPath('/')];
|
|
40
45
|
const visited = new Set<string>();
|
|
41
46
|
|
|
42
47
|
while (pending.length > 0) {
|
|
@@ -52,7 +57,7 @@ test('resolves every internal page link', async ({ page, request }) => {
|
|
|
52
57
|
|
|
53
58
|
for (const href of links) {
|
|
54
59
|
const url = new URL(href);
|
|
55
|
-
if (url.origin !== '
|
|
60
|
+
if (url.origin !== new URL(resolvePreviewUrl('/')).origin) continue;
|
|
56
61
|
|
|
57
62
|
const linkedPath = url.pathname;
|
|
58
63
|
const response = await request.get(linkedPath);
|
|
@@ -64,31 +69,31 @@ test('resolves every internal page link', async ({ page, request }) => {
|
|
|
64
69
|
});
|
|
65
70
|
|
|
66
71
|
test('serves feed, crawler, manifest, and not-found metadata', async ({ request }) => {
|
|
67
|
-
const feed = await request.get('/rss.xml');
|
|
72
|
+
const feed = await request.get(resolvePreviewPath('/rss.xml'));
|
|
68
73
|
expect(feed.ok()).toBe(true);
|
|
69
74
|
expect(await feed.text()).toContain(new URL('articles/designing-a-calm-starting-point/', site.url).href);
|
|
70
75
|
expect(await feed.text()).not.toContain('future-draft');
|
|
71
76
|
|
|
72
|
-
const robots = await request.get('/robots.txt');
|
|
77
|
+
const robots = await request.get(resolvePreviewPath('/robots.txt'));
|
|
73
78
|
expect(robots.ok()).toBe(true);
|
|
74
79
|
expect(await robots.text()).toContain(`Sitemap: ${new URL('sitemap-index.xml', site.url).href}`);
|
|
75
80
|
|
|
76
|
-
const manifest = await request.get('/site.webmanifest');
|
|
81
|
+
const manifest = await request.get(resolvePreviewPath('/site.webmanifest'));
|
|
77
82
|
expect(manifest.ok()).toBe(true);
|
|
78
83
|
expect(manifest.headers()['content-type']).toMatch(/application\/manifest\+json/u);
|
|
79
|
-
expect(await manifest.json()).toMatchObject({ name: site.name, start_url: '/' });
|
|
84
|
+
expect(await manifest.json()).toMatchObject({ name: site.name, start_url: resolvePreviewPath('/') });
|
|
80
85
|
|
|
81
|
-
const missing = await request.get('/does-not-exist/');
|
|
86
|
+
const missing = await request.get(resolvePreviewPath('/does-not-exist/'));
|
|
82
87
|
expect(missing.status()).toBe(404);
|
|
83
88
|
expect(await missing.text()).toContain('That page is not here.');
|
|
84
89
|
});
|
|
85
90
|
|
|
86
91
|
test('helps visitors recover from a missing page', async ({ page }) => {
|
|
87
|
-
const response = await page.goto('/does-not-exist/');
|
|
92
|
+
const response = await page.goto(resolvePreviewPath('/does-not-exist/'));
|
|
88
93
|
|
|
89
94
|
expect(response?.status()).toBe(404);
|
|
90
95
|
await expect(page.getByRole('heading', { level: 1, name: 'That page is not here.' })).toBeVisible();
|
|
91
96
|
await page.getByRole('link', { name: 'Return home' }).click();
|
|
92
|
-
await expect(page).toHaveURL('/');
|
|
97
|
+
await expect(page).toHaveURL(resolvePreviewUrl('/'));
|
|
93
98
|
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
|
|
94
99
|
});
|
|
@@ -2,6 +2,8 @@ import { flavors } from '@catppuccin/palette';
|
|
|
2
2
|
import { expect, test } from '@playwright/test';
|
|
3
3
|
import type { Page } from '@playwright/test';
|
|
4
4
|
|
|
5
|
+
import { previewOrigin, resolvePreviewPath } from './preview';
|
|
6
|
+
|
|
5
7
|
const toRgb = (hex: string) => {
|
|
6
8
|
const value = Number.parseInt(hex.slice(1), 16);
|
|
7
9
|
|
|
@@ -13,7 +15,7 @@ const pageBackground = async (page: Page) =>
|
|
|
13
15
|
|
|
14
16
|
test('uses Latte for light mode and Mocha for dark mode', async ({ page }) => {
|
|
15
17
|
await page.emulateMedia({ colorScheme: 'light' });
|
|
16
|
-
await page.goto('/');
|
|
18
|
+
await page.goto(resolvePreviewPath('/'));
|
|
17
19
|
await expect(page.locator('html')).toHaveCSS('background-color', toRgb(flavors.latte.colors.base.hex));
|
|
18
20
|
await expect(page.locator('main h1')).toHaveCSS('color', toRgb(flavors.latte.colors.text.hex));
|
|
19
21
|
|
|
@@ -23,7 +25,7 @@ test('uses Latte for light mode and Mocha for dark mode', async ({ page }) => {
|
|
|
23
25
|
});
|
|
24
26
|
|
|
25
27
|
test('makes every Catppuccin flavor available explicitly', async ({ page }) => {
|
|
26
|
-
await page.goto('/');
|
|
28
|
+
await page.goto(resolvePreviewPath('/'));
|
|
27
29
|
|
|
28
30
|
for (const flavor of ['latte', 'frappe', 'macchiato', 'mocha'] as const) {
|
|
29
31
|
await page.locator('html').evaluate((element, theme) => element.setAttribute('data-theme', theme), flavor);
|
|
@@ -37,11 +39,11 @@ test('serves fonts locally and presents a visible keyboard focus indicator', asy
|
|
|
37
39
|
if (request.resourceType() === 'font') fontRequests.push(request.url());
|
|
38
40
|
});
|
|
39
41
|
|
|
40
|
-
await page.goto('/');
|
|
42
|
+
await page.goto(resolvePreviewPath('/'));
|
|
41
43
|
await page.keyboard.press('Tab');
|
|
42
44
|
|
|
43
45
|
await expect(page.locator('a').first()).toBeFocused();
|
|
44
46
|
await expect(page.locator('a').first()).not.toHaveCSS('outline-style', 'none');
|
|
45
47
|
expect(fontRequests.length).toBeGreaterThan(0);
|
|
46
|
-
expect(fontRequests.every((url) => new URL(url).origin ===
|
|
48
|
+
expect(fontRequests.every((url) => new URL(url).origin === previewOrigin)).toBe(true);
|
|
47
49
|
});
|