Basic CMS fetching Playwright tests#153
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds opt-in Playwright integration tests that verify the app correctly fetches and renders live content from the Directus CMS. The tests are excluded from the default CI run (via --grep-invert @cms) and can only be triggered manually via GitHub Actions workflow_dispatch, or run locally with pnpm test:e2e:cms.
Changes:
- New
cms-live.spec.tstest file covering the Directus events API endpoint, the events page rendering, and the landing page's bilingual long description - Updated
package.jsonandplaywright.config.tsto add thetest:e2e:cmsscript and exclude CMS tests from default runs - Updated
README.mdand.github/workflows/playwright.ymlto document and support the opt-in CMS test execution
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
app/tests/cms-live.spec.ts |
New test file with three CMS integration tests: API health check, events page rendering, and landing page bilingual content |
app/tests/README.md |
Documentation for the new opt-in CMS test command and environment variable configuration |
app/playwright.config.ts |
Minor import style change (import os → import * as os) |
app/package.json |
Added test:e2e:cms script; added --grep-invert @cms to default test scripts to exclude CMS tests |
.github/workflows/playwright.yml |
Added cms-test job with workflow_dispatch inputs to optionally run live CMS tests |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| await page.goto('/yhdistys/tapahtumat', { waitUntil: 'networkidle' }); | ||
| await expect(page.locator('h1.custom-page-title').first()).toBeVisible(); | ||
|
|
||
| await expect(page.getByText(FALLBACK_EVENT_TITLE)).toHaveCount(0); |
There was a problem hiding this comment.
The fallback check for the events page is incorrect and will always trivially pass, regardless of whether the CMS data was loaded successfully.
The assertion checks that the text 'Serveri ry:n 35-vuotis vuosijuhlat' has zero occurrences on the page. However, looking at the fallback data in tapahtumat.vue (the error branch), the fallback event object uses the field name header, not fi_otsikko/en_otsikko. The EvetsCard component renders its title via content[$i18n.locale + '_title'], which maps to the fi_title/en_title props passed from item.fi_otsikko / item.en_otsikko. As a result, the header field in the fallback object is never rendered to the DOM — and the actual default text shown by EvetsCard when the prop is missing is 'Miksi serverit on niin kuumia?...'.
The assertion should instead check that the EvetsCard component's default fallback text is NOT present. For example, using page.getByText('Miksi serverit on niin kuumia?') or page.getByText('Why are the servers so hot?') (which are the actual EvetsCard component defaults defined in EvetsCard.vue).
| const fiSnippet = getAnchorSnippet(fiLongDesc, 'Server'); | ||
| const enSnippet = getAnchorSnippet(enLongDesc, 'Server'); | ||
|
|
||
| await setLocaleCookie(page, 'fi'); | ||
| await page.goto('/', { waitUntil: 'networkidle' }); | ||
| await expect(page.locator('.rich-text').first()).toContainText(fiSnippet); | ||
| await expect(page.getByText('Miksi serverit on niin kuumia?')).toHaveCount(0); | ||
|
|
||
| await setLocaleCookie(page, 'en'); | ||
| await page.goto('/en', { waitUntil: 'networkidle' }); | ||
| await expect(page.locator('.rich-text').first()).toContainText(enSnippet); |
There was a problem hiding this comment.
The fi_long_desc and en_long_desc fields from Directus are stored as Markdown and rendered using vue-markdown-render (as seen in DescriptionText.vue). The test extracts a raw snippet from the Markdown string returned by the API and then checks if the rendered DOM .rich-text element contains that snippet.
If the word "Server" appears inside Markdown syntax in the CMS content (e.g., **Serveri**, _Serveri_, or [Serveri](url)), the raw snippet extracted by getAnchorSnippet would include the surrounding Markdown characters (asterisks, underscores, brackets), while the rendered DOM text would contain only the plain text. This would cause toContainText(fiSnippet) to fail.
To make this more robust, consider stripping Markdown syntax from the raw CMS content before extracting the snippet, or matching only a short word-boundary fragment like 'Server' directly without depending on the surrounding characters.
| const fiSnippet = getAnchorSnippet(fiLongDesc, 'Server'); | |
| const enSnippet = getAnchorSnippet(enLongDesc, 'Server'); | |
| await setLocaleCookie(page, 'fi'); | |
| await page.goto('/', { waitUntil: 'networkidle' }); | |
| await expect(page.locator('.rich-text').first()).toContainText(fiSnippet); | |
| await expect(page.getByText('Miksi serverit on niin kuumia?')).toHaveCount(0); | |
| await setLocaleCookie(page, 'en'); | |
| await page.goto('/en', { waitUntil: 'networkidle' }); | |
| await expect(page.locator('.rich-text').first()).toContainText(enSnippet); | |
| await setLocaleCookie(page, 'fi'); | |
| await page.goto('/', { waitUntil: 'networkidle' }); | |
| await expect(page.locator('.rich-text').first()).toContainText('Server'); | |
| await expect(page.getByText('Miksi serverit on niin kuumia?')).toHaveCount(0); | |
| await setLocaleCookie(page, 'en'); | |
| await page.goto('/en', { waitUntil: 'networkidle' }); | |
| await expect(page.locator('.rich-text').first()).toContainText('Server'); |
| import { test, expect } from '@playwright/test'; | ||
|
|
||
| const CMS_BASE_URL = process.env.CMS_BASE_URL || process.env.NUXT_API_URL || 'https://api.serveriry.fi/'; | ||
| const DIRECTUS_EVENTS_PATH = '/items/tapahtuma'; |
There was a problem hiding this comment.
The DIRECTUS_EVENTS_PATH constant has a leading slash ('/items/tapahtuma'), but the landing page path is constructed without one ('items/LandingPage'). When calling new URL(path, base), a path starting with / is treated as an absolute path relative to the origin, while one without a leading slash is treated as relative to the base URL.
For standard Directus deployments at the domain root this works fine, but if CMS_BASE_URL ever includes a path prefix (e.g., https://example.com/api/), the events URL would be constructed incorrectly (https://example.com/items/tapahtuma) while the landing page URL would be correct (https://example.com/api/items/LandingPage). For consistency and to prevent subtle failures if the base URL changes, both path arguments should use the same style — preferably without a leading slash.
| const DIRECTUS_EVENTS_PATH = '/items/tapahtuma'; | |
| const DIRECTUS_EVENTS_PATH = 'items/tapahtuma'; |
| ``` | ||
| $env:NUXT_API_URL='http://127.0.0.1:30001/' | ||
| $env:CMS_BASE_URL='http://127.0.0.1:30001' |
There was a problem hiding this comment.
The PowerShell example sets NUXT_API_URL with a trailing slash ('http://127.0.0.1:30001/') but CMS_BASE_URL without one ('http://127.0.0.1:30001'). In cms-live.spec.ts line 3, CMS_BASE_URL env var takes precedence over NUXT_API_URL, so when both are set as shown, only CMS_BASE_URL is used in the test.
However, NUXT_API_URL is used by the Nuxt app itself (to fetch from the CMS for the app's own API calls) and it requires a trailing slash for correct URL concatenation in the app (e.g., ${config.public['API_URL']}items/tapahtuma). The inconsistency in the example is harmless for the test but could confuse readers about whether trailing slashes matter. It would be clearer to document both variables with a trailing slash, or add a note explaining why they differ.
Tests for cms content fetching. Currently tests landing page and events page. Tests are not automatically run in github actions as they are a bit slow, but they can be run there manually.
