@rm-industries/create-forge 0.4.0-beta.4 → 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.
@@ -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: 'http://127.0.0.1:4321',
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: 'http://127.0.0.1:4321',
29
+ url: resolvePreviewUrl('/'),
28
30
  reuseExistingServer: !process.env.CI,
29
31
  },
30
32
  });
@@ -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
- await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', new URL(route, site.url).href);
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', new URL(route, site.url).href);
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.locator('a[href="/articles/designing-a-calm-starting-point/"]').getByLabel('Article tags'),
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 !== 'http://127.0.0.1:4321') continue;
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 === 'http://127.0.0.1:4321')).toBe(true);
48
+ expect(fontRequests.every((url) => new URL(url).origin === previewOrigin)).toBe(true);
47
49
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rm-industries/create-forge",
3
- "version": "0.4.0-beta.4",
3
+ "version": "0.4.0-beta.5",
4
4
  "description": "Create a content-driven Forge website",
5
5
  "keywords": [
6
6
  "astro",