Skip to content
Draft
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
58 changes: 58 additions & 0 deletions .github/workflows/playwright.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
on: pull_request
name: Playwright Tests
jobs:
playwright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Install site
run: |
docker network create frontend
docker network create serviceplatformen_organisation_api_app
docker compose pull
docker compose up --detach
# Important: Use --no-interaction to make https://getcomposer.org/doc/06-config.md#discard-changes have effect.
docker compose exec --user root phpfpm composer install --no-interaction
# Install the site
docker compose exec --user root phpfpm vendor/bin/drush site:install --existing-config --yes
# Download and install external libraries
docker compose exec --user root phpfpm vendor/bin/drush webform:libraries:download
# Build theme assets
docker compose run --rm node yarn --cwd /app/web/themes/custom/os2forms_selvbetjening_theme install
docker compose run --rm node yarn --cwd /app/web/themes/custom/os2forms_selvbetjening_theme build
# Open the site
echo $(docker compose exec phpfpm vendor/bin/drush --uri=http://$(docker compose port nginx 8080) user:login)


- name: Install dependencies
run: yarn install

- name: Install Playwright Browsers
run: docker compose run --rm playwright yarn playwright install --with-deps

- name: Run Playwright tests
run: |
docker compose run --rm playwright yarn playwright test --update-snapshots --retries 3 | tee output.log
if grep -q -e "Error: A snapshot doesn't exist at" -e "Screenshot comparison failed" output.log; then
echo "Playwright tests failed due to a snapshot issue."
echo "SNAPSHOT_DIFFERENCES=true" >> $GITHUB_ENV
exit 1
elif grep -q "failed" output.log; then
echo "Playwright tests failed due to a non-snapshot issue."
exit 1
fi
- uses: actions/upload-artifact@v4
id: artifact-upload
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- name: Comment on PR with report link
uses: thollander/actions-comment-pull-request@v3
if: ${{ failure() && env.SNAPSHOT_DIFFERENCES == 'true' }}
with:
message: |
### Playwright visual snapshot differences were detected.
View the [Playwright report](${{ steps.artifact-upload.outputs.artifact-url }}) to review the visual differences.
**To approve the snapshot changes and update the snapshots, please comment:** /approve-snapshots
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,9 @@ config/sync/**/leaflet_layers.map_layer.*
config/sync/os2web_nemlogin.settings.yml
# Ignore attachment components
config/sync/os2forms_attachment.os2forms_attachment_component.*.yml

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
5 changes: 4 additions & 1 deletion docker-compose.override.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ networks:
external: true
name: serviceplatformen_organisation_api_app

include:
- docker-compose.playwright.yml

services:
phpfpm:
networks:
Expand All @@ -13,7 +16,7 @@ services:
- PHP_MEMORY_LIMIT=512M

node:
image: node:16
image: node:22
profiles:
- dev
networks:
Expand Down
15 changes: 15 additions & 0 deletions docker-compose.playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
services:
playwright:
# https://playwright.dev/docs/docker
# This Playwright version should match the one in `package.json`.
image: mcr.microsoft.com/playwright:v1.51.1
profiles:
- test
networks:
- app
depends_on:
- nginx
volumes:
- .:/app
- /tmp/.X11-unix:/tmp/.X11-unix
working_dir: /app
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"license": "UNLICENSED",
"private": true,
"devDependencies": {
"@playwright/test": "^1.51.1",
"@types/node": "^22.13.9",
"markdownlint-cli": "^0.31.1"
},
"scripts": {
Expand Down
87 changes: 87 additions & 0 deletions playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// @ts-check
import { defineConfig, devices } from '@playwright/test';

/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });

/**
* @see https://playwright.dev/docs/test-configuration
*/
export default defineConfig({
testDir: './playwright',
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://nginx:8080',

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},

/* Configure projects for major browsers */
projects: [
{
name: 'hest',
testMatch: /global\.setup\.js/,
},
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['hest'],
},
/*
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
dependencies: ['hest'],
},

{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
dependencies: ['hest'],
},*/

/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },

/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],

/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});
54 changes: 54 additions & 0 deletions playwright/example.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// @ts-check
import { test, expect } from '@playwright/test';
import { SetupWebform } from './fixtures/SetupWebform';
import { AddElementToWebform } from './fixtures/AddElementToWebform';

const runnerTimestamp = new Date().getTime();

test.describe('Login flow, create webform', () => {
let authenticatedContext;

test.beforeAll('1. Login', async ({ browser }) => {

const context = await browser.newContext();
const page = await context.newPage();

await page.goto('/da/user/login');

await expect(page.locator('input[name="name"]')).toBeVisible();
await page.fill('input[name="name"]', 'playwright');
await page.fill('input[name="pass"]', 'playwrightt3st01');

await page.click('input[name="op"]');

await expect(page.locator('#block-gin-page-title > h1')).toHaveText('playwright');

authenticatedContext = context;

await page.close();
});

test('2. Create new webform', async () => {
const page = await authenticatedContext.newPage();
const webform = new SetupWebform(page, runnerTimestamp);

await webform.goto();
await webform.openCreateNewWebformDialog();
await webform.createNewWebform();
})

test('3. Configure textfield element', async ({} , testInfo) => {
const page = await authenticatedContext.newPage();
const addElementToWebform = new AddElementToWebform(page, runnerTimestamp);

await addElementToWebform.addElement('textfield');

const customersScreenshot = await page.screenshot();
await testInfo.attach('Customers Page', {
body: customersScreenshot,
contentType: 'image/png',
});

})

});
20 changes: 20 additions & 0 deletions playwright/fixtures/AddElementToWebform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { expect } from '@playwright/test';
import { GlobalSettings } from './GlobalSettings';

export class AddElementToWebform {
constructor(page, runnerTimestamp) {
this.page = page;
this.webformName = GlobalSettings.webformName + runnerTimestamp;
this.titleInput = this.page.locator('div.js-form-item-properties-title > input[name="properties[title]"]');
}

async addElement(elementName) {
await this.page.goto(`da/admin/structure/webform/manage/${this.webformName}/element/add/${elementName}`);
await expect(this.page.locator('#block-gin-page-title > h1')).toHaveText('Tilføj Tekstfelt element');
await this.titleInput.fill(elementName);
await this.page.click('div.form-actions > input.form-submit');

await expect(this.page.locator(`a#edit-webform-ui-elements-${elementName}-title-link`)).toBeVisible();
}

}
4 changes: 4 additions & 0 deletions playwright/fixtures/GlobalSettings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export class GlobalSettings {
static affiliationName = 'playwright_affiliation';
static webformName = 'playwright_webform';
}
28 changes: 28 additions & 0 deletions playwright/fixtures/SetupWebform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { expect } from '@playwright/test';
import { GlobalSettings } from './GlobalSettings';

export class SetupWebform {
constructor(page, runnerTimestamp) {
this.page = page;
this.webformName = GlobalSettings.webformName + runnerTimestamp;
this.titleInput = this.page.locator('div.js-form-item-title > input[name="title"]');
this.createWebformButton = this.page.locator('div.form-actions > button.form-submit');
}

async goto() {
await this.page.goto('/da/admin/structure/webform');
await expect(this.page.locator('#block-gin-page-title > h1')).toHaveText('Webformularer');
}

async openCreateNewWebformDialog(title) {
await expect(this.page.locator('a[href="/da/admin/structure/webform/add"]')).toBeVisible();
await this.page.click('a[href="/da/admin/structure/webform/add"]');
await expect(this.page.locator('div[role="dialog"].ui-dialog')).toBeVisible();
}

async createNewWebform() {
await this.titleInput.fill(this.webformName);
await this.createWebformButton.click();
await expect(this.page.locator('div#block-gin-page-title > h1')).toHaveText(this.webformName);
}
}
65 changes: 65 additions & 0 deletions playwright/global.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// @ts-check
import { test as baseTest, expect } from '@playwright/test';
import { GlobalSettings } from './fixtures/GlobalSettings';

const test = baseTest.extend({
authenticatedContext: async ({ browser }, use) => {
// Create a new browser context
const context = await browser.newContext();
const page = await context.newPage();
const affiliation = GlobalSettings.affiliationName;

try {
// Perform login
await page.goto('/da/user/login');
await page.fill('input[name="name"]', 'playwright');
await page.fill('input[name="pass"]', 'playwrightt3st01');
await page.click('input[name="op"]');

// Ensure login is successful
await expect(page.locator('h1.page-title').filter({hasText: 'playwright'})).toBeVisible();
console.log('Successfully logged in.');

// Navigate to overview and check if affiliation exists
await page.goto('da/admin/structure/taxonomy/manage/user_affiliation/overview');
const affiliationExists = await page.locator('table#taxonomy a').filter({ hasText: affiliation }).isVisible();

// Create an affiliation if it does not exist
if (!affiliationExists) {
await page.goto('da/admin/structure/taxonomy/manage/user_affiliation/add');
await page.fill('input[name="name[0][value]"]', affiliation);
await page.click('input[data-drupal-selector="edit-overview"]');

// Verify creation of affiliation
await expect(page.locator('table#taxonomy a').filter({ hasText: affiliation })).toBeVisible();
console.log('Successfully created affiliation.');
} else {
console.log('Affiliation already exists.');
}

// Assign affiliation to user
await page.goto('da/admin/people');
await expect(page.locator('td.views-field-name > a').filter({hasText: 'playwright'})).toBeVisible();
await expect(page.locator('td.views-field-name > a').filter({hasText: 'playwright'})).toHaveAttribute('href');
const userUrl = await page.locator('td.views-field-name > a').filter({hasText: 'playwright'}).getAttribute('href');

await page.goto(userUrl + '/edit?destination=/da/admin/people');

await expect(page.locator('div.js-form-item-terms-user-affiliation > select#edit-terms-user-affiliation > option').filter({hasText: affiliation})).toBeDefined();
await page.locator('div.js-form-item-terms-user-affiliation > select#edit-terms-user-affiliation > option').filter({hasText: affiliation}).click();
await page.locator('div#edit-actions > input#edit-submit').click();

console.log('Successfully assigned affiliation.');
// Provide the authenticated context to dependent tests
await use(context);
} finally {
console.log('Basic setup complete.');
}
},
});

test('Global setup - Create authenticated context', async ({ authenticatedContext }) => {
console.log('Authenticated context is ready for dependent tests.');
});

module.exports = { test };
Loading
Loading