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
54 changes: 54 additions & 0 deletions .github/workflows/prepublish-live.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Prepublish live Siteimprove test

on:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: siteimprove-live-test
cancel-in-progress: false

jobs:
live:
# Reviewed workflow code only. Environment approval is required separately.
if: github.repository == 'Siteimprove/CMS-plugin-Wordpress' && github.ref == 'refs/heads/master'
environment: siteimprove-test
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Check out the reviewed PR 64 plugin
uses: actions/checkout@v6
with:
ref: a23af8519861f674d700cbe4fe183817f47458dc
path: .plugin-under-test
sparse-checkout: siteimprove
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
- run: npm ci
- run: npm run test:live:contracts
- run: npx playwright install --with-deps chromium
- run: node scripts/prepare-live-env.js
- run: npm run env:start
- name: Verify Live page data and one fresh Prepublish issue
run: npm run test:live
env:
SITEIMPROVE_RUN_LIVE: '1'
SITEIMPROVE_PUBLIC_URL: ${{ secrets.SITEIMPROVE_PUBLIC_URL }}
SITEIMPROVE_CRAWLED_URL: ${{ secrets.SITEIMPROVE_CRAWLED_URL }}
SITEIMPROVE_API_USERNAME: ${{ secrets.SITEIMPROVE_API_USERNAME }}
SITEIMPROVE_API_KEY: ${{ secrets.SITEIMPROVE_API_KEY }}
SITEIMPROVE_USERNAME: ${{ secrets.SITEIMPROVE_USERNAME }}
SITEIMPROVE_PASSWORD: ${{ secrets.SITEIMPROVE_PASSWORD }}
DEBUG: ''
PWDEBUG: '0'
- run: npm run env:stop
if: ${{ always() }}
# No screenshots, traces, browser state, server logs or account data uploads.
11 changes: 8 additions & 3 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@ the missing-title result. This concerns page-content fidelity, not SDK UI stylin

### What remains for an actual Siteimprove end-to-end test

A first manual live workflow and runner are now prepared in
[tests/live/README.md](tests/live/README.md). They have not been authenticated or
run against Siteimprove. The following account and network requirements still
apply; PR #65’s existing tests continue to use no secrets.

We still need a Siteimprove test account with Prepublish access and a known
crawled page accessible to both the browser user and API user. The plugin's
credential validation compares **Public URL** with the sites available to the
Expand Down Expand Up @@ -276,9 +281,9 @@ First configure and validate the real integration in that environment:
of source files, logs, reports and uploaded traces. Local browser state belongs
in the ignored `playwright/.auth/` directory.

The full scan test and its authenticated GitHub job have **not** been implemented
or run yet: the account, registered site and actual overlay/login flow are needed
to implement and verify those steps. The integration workflow must not be interpreted
The initial live runner and authenticated GitHub job are **prepared but unverified
against the account**. The first authorized run must verify the actual login,
site mapping and fresh scan results. The integration workflow must not be interpreted
as proof that a Siteimprove scan passed.

The revision fixture uses [WordPress’s autosave API](https://developer.wordpress.org/reference/functions/wp_create_post_autosave/)
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
"env:stop": "node scripts/wordpress-env.js stop",
"env:status": "node scripts/wordpress-env.js status",
"test:wordpress": "playwright test --config=playwright.wordpress.config.js",
"test:wordpress:report": "playwright show-report playwright-wordpress-report"
"test:wordpress:report": "playwright show-report playwright-wordpress-report",
"test:live": "node tests/live/run.js",
"test:live:contracts": "node --test tests/live/*.test.js",
"test:live:fixture": "playwright test --config=playwright.live-fixture.config.js"
},
"devDependencies": {
"@playwright/test": "1.63.0",
Expand Down
8 changes: 8 additions & 0 deletions playwright.live-fixture.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { defineConfig } = require('@playwright/test');
module.exports = defineConfig({
testDir:'./tests/live', testMatch:['fixture.spec.js','observer.spec.js'], timeout:60000, workers:1,
outputDir:'./test-results/live-fixture', retries:0, forbidOnly:Boolean(process.env.CI),
reporter:[['list',{printSteps:true}]],
projects:[{name:'chromium',use:{browserName:'chromium'}},{name:'firefox',testIgnore:'observer.spec.js',use:{browserName:'firefox'}}],
use:{baseURL:'http://localhost:8888',trace:'off',screenshot:'off',video:'off'},
});
9 changes: 9 additions & 0 deletions scripts/prepare-live-env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const fs = require('node:fs');
// Contains paths and flags only; secret values are supplied to the runner later.
fs.writeFileSync('.wp-env.override.json', JSON.stringify({
plugins:['./.plugin-under-test/siteimprove'],
config:{ WP_DEBUG:false, WP_DEBUG_LOG:false, WP_DEBUG_DISPLAY:false,
SITEIMPROVE_TEST_ENVIRONMENT:false, SITEIMPROVE_TEST_MOCK_SERVICE:false,
SITEIMPROVE_LIVE_TEST_ENVIRONMENT:true },
mappings:{'wp-content/mu-plugins':'./tests/live/support'},
}, null, 2));
9 changes: 5 additions & 4 deletions tests/SCENARIOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,11 @@ is still unverified. The suite has 18 tests including readiness in each browser.

## Planned: authenticated Siteimprove checks

These scenarios are **not implemented or verified yet**. They require account
access, working authentication and URL mapping. The missing-title fixture also
still needs to be added. Run the small checks across selected WordPress setups;
the environment matrix is described in [TESTING.md](../TESTING.md).
An initial runner and missing-title fixture are now prepared in
[the live-test follow-up](live/README.md), but the authenticated flow is **not
verified against the account**. It requires an approved manual run to validate
login, URL mapping and real results. Start with one Chromium configuration;
additional live environments remain future coverage.

### Retrieve existing Live page data

Expand Down
125 changes: 125 additions & 0 deletions tests/live/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Manual live Siteimprove test

Status: the workflow and initial test implementation are prepared, but no
GitHub run, authenticated login or real Prepublish scan has been performed.
The first approved run must validate the account, network access and SDK UI
selectors. A failure is not converted into a skip or a pass.

This follow-up builds on the credential-free infrastructure in PR #65. It adds
one Chromium smoke test with two outcomes: existing Live page data for the exact
crawled URL, followed by a newly completed Prepublish check reporting a missing
HTML title in the current local draft. It does not test SDK layout or highlighting.
Report-rerender styling remains a separate manual acceptance check.

## Prerequisites

The `siteimprove-test` GitHub Environment must contain:

- `SITEIMPROVE_PUBLIC_URL`: the base URL configured in the WordPress plugin.
- `SITEIMPROVE_CRAWLED_URL`: one exact crawled page under that base URL.
- `SITEIMPROVE_API_USERNAME` and `SITEIMPROVE_API_KEY`.
- `SITEIMPROVE_USERNAME` and `SITEIMPROVE_PASSWORD`: a direct-login test user.

The account needs existing Prepublish access, and both users must have access
to the mapped site. Initial terms acceptance must already be complete. The test
does not accept terms, activate a subscription or bypass MFA/CAPTCHA. It currently
uses English SDK labels; those labels and the missing-title issue name need
confirmation in the account during the first approved run.

The runner must reach the Siteimprove API, SDK and identity services. The crawled
website itself is not visited: its URL is used as the plugin's normal mapping
context. Whether an unrelated existing crawled site can serve as that context
for this local fixture remains unverified with the real service.

## What the test does

1. Check that the API reports Prepublish as ready.
2. Sign into the disposable WordPress installation.
3. Configure Public URL, the latest SDK experience and API credentials using
the plugin's settings form and normal credential validation.
4. Create a local published control and an unpublished autosave with a unique
marker. The fixture's permalink matches the crawled page path; the plugin
performs its own Public URL transformation without request rewriting.
5. Confirm the preview contains the unique marker and exactly one empty title,
that the plugin reports the expected public URL, and that an anonymous
visitor cannot retrieve the draft content.
6. Sign into Siteimprove through the real plugin's popup using direct login.
7. Require an authenticated SDK polling response for the exact crawled URL,
with existing Live page data, and open Live page view.
8. Start a new check from Prepublish view. Observe its running state and the
real SDK's `contentcheck-flat-dom` message, without replacing the SDK queue
or altering the content. The message must contain this run's draft marker.
9. Require completion of that new check and the missing-title issue in
Prepublish view. Historical crawl results cannot satisfy this assertion.

The title is removed in the server-rendered preview template, including the
plugin's capture iframe. Clearing only the editor title or mutating the outer
browser document would not exercise the same path.

## Execution and approval

The workflow is `prepublish-live.yml` (**Prepublish live Siteimprove test**).
It has only `workflow_dispatch`, runs only from `master` in this repository,
and uses the `siteimprove-test` environment's approval rules. It cannot run
from a PR branch. Workflow availability requires merging it into the default
branch; neither creating the PR nor adding secrets starts it.

The plugin is pinned to reviewed PR #64 commit
`a23af8519861f674d700cbe4fe183817f47458dc`. There is deliberately no arbitrary
plugin-ref input in this secret-bearing job. Changing the plugin commit requires
a reviewed workflow change. The ordinary credential-free workflows retain their
flexible `plugin_ref` inputs.

No workflow has been dispatched. Review this implementation before an authorized
maintainer starts and approves the first live run.

## Diagnostics and secrets

The live runner prints only fixed phase names and pass/fail outcomes. It catches
raw browser and API errors instead of printing URLs, field values, headers or
account content. Screenshots, traces, video, saved browser sessions and server-log
uploads are disabled. WordPress debug logging is disabled. Secrets are supplied
only to the live test step, after environment approval; no secret values are
written into tracked configuration files.

Failures identify a phase such as API entitlement, login, URL mapping or new
Prepublish completion. The tradeoff is less detail for investigating SDK changes;
raw account diagnostics must not be published as public PR artifacts. This policy
is separate from the screenshots enabled for the synthetic tests in PR #65.

## Credential-free validation

`npm run test:live:contracts` verifies URL mapping, required configuration and
recognition of real Live page data versus login/missing-page responses. It also
checks both login failure paths for unhandled event rejections and guards against
adding screenshot/trace/video/session capture or artifact uploads to the current
live runner and workflow.

The PHP fixture can be checked locally without any Siteimprove credentials:

```sh
npm run env:stop
node scripts/prepare-live-env.js
npm run env:start
npm run test:live:fixture
```

These fixture tests block external browser requests and use synthetic content.
They require the ignored `.plugin-under-test/siteimprove` checkout, as used by
the manual workflows. They verify root and nested paths, query preservation,
real WordPress autosaves, empty titles, inline styles and anonymous access checks.
The WordPress fixture runs in Chromium and Firefox. A synthetic cross-origin
observer check runs in Chromium, matching the live runner. The same observer
check failed in Firefox because iframe initialization did not install the
DOMContentLoaded observer on the final document; Firefox live instrumentation
is not validated. These checks do not validate live login, entitlement or scans.

The live runner refuses to proceed unless `SITEIMPROVE_RUN_LIVE=1` and all six
secrets are supplied. Do not enable that flag merely to validate fixture code.

## Sources for the initial integration

- [WordPress plugin setup](https://help.siteimprove.com/support/solutions/articles/80001079368-new-wordpress-plugin)
- [Prepublish workflow](https://help.siteimprove.com/support/solutions/articles/80001077559-how-to-run-a-prepublish-check-with-the-new-plugin-ui)
- [Public SDK loader](https://cdn.siteimprove.net/cms/overlay-latest.js) — inspected version 2.1.3130.1 for iframe, polling and message contracts.
- The public identity form was inspected without entering credentials; its first step uses `loginId` and a Continue button. The subsequent password step still needs authenticated-flow verification.
46 changes: 46 additions & 0 deletions tests/live/fixture.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const { test, expect } = require('@playwright/test');
const { randomUUID } = require('node:crypto');

// Synthetic data only: validates the new PHP fixture, not a live Siteimprove scan.
for (const fixture of [{path:'/test-live/story.html',query:'lang=en'}, {path:'/',query:''}]) {
test(`The live fixture preserves ${fixture.path} and renders a private, styled preview with no title`, async ({ page, browser }) => {
await page.route('**/*', route => new URL(route.request().url()).hostname === 'localhost' ? route.continue() : route.abort());
await page.goto('/wp-login.php');
await expect(page.locator('#user_login')).toBeFocused();
await page.locator('#user_login').fill('admin');
await page.locator('#user_pass').fill('password');
await page.locator('#wp-submit').click();
await expect(page).toHaveURL(/\/wp-admin\//);
const marker = `SI-LIVE-${randomUUID()}`;
const urls = await test.step('Given a published control and a distinct unpublished autosave at the mapped local path', async () => {
await page.goto('/wp-admin/tools.php?page=siteimprove-live-fixture');
await page.locator('[name=fixture_path]').fill(fixture.path);
await page.locator('[name=fixture_query]').fill(fixture.query);
await page.locator('[name=fixture_marker]').fill(marker);
await page.getByRole('button',{name:'Create live test fixture'}).click();
return {preview:await page.locator('#live-preview').getAttribute('href'), published:await page.locator('#live-published').getAttribute('href')};
});
await test.step('Then the mapped path and query are preserved and the preview has an empty title', async () => {
expect(new URL(urls.published).pathname).toBe(fixture.path);
expect(new URL(urls.published).search.slice(1)).toBe(fixture.query);
await page.goto(urls.preview);
expect(new URL(page.url()).pathname).toBe(fixture.path);
await expect(page.locator('head > title')).toHaveCount(1);
await expect(page).toHaveTitle('');
await expect(page.getByText(marker,{exact:true})).toBeVisible();
await expect(page.getByText('SI-LIVE-PUBLISHED-CONTROL',{exact:true})).toHaveCount(0);
await expect(page.locator('#si-live-style')).toHaveCSS('color','rgb(20, 40, 60)');
});
await test.step('And an anonymous visitor sees only the published control', async () => {
const anonymous = await browser.newContext();
try {
const published = await anonymous.request.get(urls.published);
expect(await published.text()).toContain('<title>Live test published control</title>');
expect(await published.text()).toContain('SI-LIVE-PUBLISHED-CONTROL');
expect(await published.text()).not.toContain(marker);
const preview = await anonymous.request.get(urls.preview);
expect(await preview.text()).not.toContain(marker);
} finally { await anonymous.close(); }
});
});
}
31 changes: 31 additions & 0 deletions tests/live/observer.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const { test, expect } = require('@playwright/test');
const { randomUUID } = require('node:crypto');

// The live runner uses Chromium; this validates its browser instrumentation.
test('Read-only observers recognize a captured preview and its cross-origin SDK handoff', async ({ browser }) => {
const { observeCapture } = require('./run');
const context = await browser.newContext();
const evidence = {};
const marker = `SI-LIVE-${randomUUID()}`;
try {
await observeCapture(context, marker, evidence);
const page = await context.newPage();
// Synthetic HTTP documents; no request reaches WordPress or Siteimprove.
await page.route('**/*', route => route.fulfill({contentType:'text/html',body:`<!doctype html><html><head><title></title></head><body>${marker}</body></html>`}));
await page.goto('http://localhost:8888/observer-parent');
await page.evaluate(async () => {
for (const url of ['http://localhost:8888/observer-preview?si_preview_nonce=synthetic', 'https://contentassistant.eu.siteimprove.com/observer']) {
const frame = document.createElement('iframe');
const loaded = new Promise(resolve => { frame.onload = resolve; });
frame.src = url;
document.body.appendChild(frame);
await loaded;
}
});
await expect.poll(() => evidence.preview).toEqual({kind:'preview',marker:true,emptyTitle:true,excludesPublished:true,excludesPlugin:true});
await page.evaluate(marker => {
window.frames[1].postMessage({si:'contentcheck-flat-dom',data:{dom:{strings:[marker,'rgb(20, 40, 60)']}}},'https://contentassistant.eu.siteimprove.com');
},marker);
await expect.poll(() => evidence.handoff).toEqual({kind:'handoff',marker:true,style:true,excludesPublished:true});
} finally { await context.close(); }
});
Loading