Skip to content

Commit

Permalink
Run playwright tests in our gha integration test workflow (#1132)
Browse files Browse the repository at this point in the history
* run playwright tests after S&R tests in the gha k8s instance

* ensure maildev is forwarded properly

* install dependancies during playwright setup

* fix email base url for gha environment

* fix regex used for AddMemberModal tests

* ensure return to path used in LoginRedirect is relative for invite emails

* Exclude viewer page playwright tests in GHA
because Mongo is not available

* Run Playwright tests even if dotnet tests fail

* use develop image tag for lexbox-hgweb and fw-headless in gha integration tests

* remove unused environment variables from frontend tests

* pull playwright tests into a reusable action and call them from both staging and gha integration tests

---------

Co-authored-by: Tim Haasdyk <[email protected]>
  • Loading branch information
hahn-kev and myieye authored Nov 19, 2024
1 parent 022f8b5 commit c23a0bc
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 141 deletions.
55 changes: 55 additions & 0 deletions .github/actions/playwright-tests/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Setup and run playwright tests
inputs:
lexbox-hostname:
description: 'The hostname of the lexbox server, should include port if not 80'
required: true
lexbox-default-password:
description: 'The default password for the lexbox server'
required: true
viewer-tests:
description: 'Whether to run viewer tests'
required: false
default: 'true'

runs:
using: composite
steps:
# First we need to setup Node...
- uses: actions/setup-node@v4
with:
node-version-file: 'frontend/package.json'
# Then we can set up pnpm...
- uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0
with:
package_json_file: 'frontend/package.json'
# Then we can have Node set up package caching
- uses: actions/setup-node@v4
with:
node-version-file: 'frontend/package.json'
cache: 'pnpm'
cache-dependency-path: 'frontend/pnpm-lock.yaml'
- name: Playwright setup
shell: bash
working-directory: frontend
run: pnpm install
- name: Set up Playwright dependencies
shell: bash
working-directory: frontend
run: pnpm exec playwright install --with-deps

- name: Integration tests (Playwright)
id: playwright-tests
shell: bash
env:
TEST_SERVER_HOSTNAME: ${{ inputs.lexbox-hostname }}
TEST_DEFAULT_PASSWORD: ${{ inputs.lexbox-default-password }}
working-directory: frontend
run: pnpm run test ${{ inputs.viewer-tests != 'true' && '-g "^(?!.*Viewer Page).*"' || '' }}

- name: Upload playwright results
if: ${{ always() && steps.playwright-tests.outcome != 'skipped' }}
uses: actions/upload-artifact@v4
with:
name: playwright-traces
path: |
./frontend/test-results
14 changes: 13 additions & 1 deletion .github/workflows/integration-test-gha.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ jobs:
kubectl wait --for=condition=Ready --timeout=120s pod -l 'app.kubernetes.io/component=controller' -n languagedepot
kubectl wait --for=condition=Ready --timeout=120s pod -l 'app in (lexbox, ui, hg, db)' -n languagedepot
- name: forward ingress
run: kubectl port-forward service/ingress-nginx-controller 6579:80 -n languagedepot &
run: |
kubectl port-forward service/ingress-nginx-controller 6579:80 -n languagedepot &
kubectl port-forward service/lexbox 1080:1080 -n languagedepot &
- name: verify ingress
run: curl -v http://localhost:6579
- name: build
Expand All @@ -89,6 +91,16 @@ jobs:
TEST_PROJECT_CODE: 'sena-3'
TEST_DEFAULT_PASSWORD: 'pass'
run: dotnet test LexBoxOnly.slnf --logger GitHubActions --filter "Category=Integration|Category=FlakyIntegration" --blame-hang-timeout 40m

##playwright tests
- name: Setup and run playwright tests
if: ${{ !cancelled() }}
uses: ./.github/actions/playwright-tests
with:
lexbox-hostname: 'localhost:6579'
lexbox-default-password: 'pass'
viewer-tests: 'false' # exclude the viewer page tests, because mongo is not available

- name: status
if: failure()
run: |
Expand Down
44 changes: 9 additions & 35 deletions .github/workflows/integration-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,6 @@ jobs:
runs-on: ${{ inputs.runs-on }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Setup self-hosted dependencies
if: ${{ inputs.runs-on == 'self-hosted' }}
run: |
Expand All @@ -164,47 +162,23 @@ jobs:
sudo apt-get install -f
rm powershell_7.4.1-1.deb_amd64.deb
pwsh #validate that powershell installed correctly
# First we need to setup Node...
- uses: actions/setup-node@v4
with:
node-version-file: 'frontend/package.json'
# Then we can set up pnpm...
- uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0
with:
package_json_file: 'frontend/package.json'
# Then we can have Node set up package caching
- uses: actions/setup-node@v4
with:
node-version-file: 'frontend/package.json'
cache: 'pnpm'
cache-dependency-path: 'frontend/pnpm-lock.yaml'
- name: Playwright setup
working-directory: frontend
run: pnpm install && pnpm pretest
- name: Set up Playwright dependencies
working-directory: frontend
if: ${{ inputs.runs-on == 'self-hosted' }}
run: sudo pnpm exec playwright install-deps
- name: Integration tests (Playwright)
env:
TEST_SERVER_HOSTNAME: ${{ vars.TEST_SERVER_HOSTNAME }}
# this is not a typo, we need to use the lf domain because it has a cert that hg will validate
TEST_STANDARD_HG_HOSTNAME: ${{ vars.TEST_STANDARD_HG_HOSTNAME }}
TEST_RESUMABLE_HG_HOSTNAME: ${{ vars.TEST_RESUMABLE_HG_HOSTNAME }}
TEST_PROJECT_CODE: 'sena-3'
TEST_DEFAULT_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
working-directory: frontend
run: pnpm test
- name: Setup and run playwright tests
if: ${{ !cancelled() }}
uses: ./.github/actions/playwright-tests
with:
lexbox-hostname: ${{ vars.TEST_SERVER_HOSTNAME }}
lexbox-default-password: ${{ secrets.TEST_USER_PASSWORD }}
viewer-tests: 'true'
- name: Password protect Playwright traces
id: password_protect_test_results
if: ${{ always() }}
if: ${{ !cancelled() }}
shell: bash
env:
ZIP_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
run: 7z a ./playwright-traces.7z -mx=0 -mmt=off ./frontend/test-results -p"$ZIP_PASSWORD"
- name: Upload playwright results
if: ${{ always() && steps.password_protect_test_results.outcome == 'success' }}
if: ${{ !cancelled() && steps.password_protect_test_results.outcome == 'success' }}
uses: actions/upload-artifact@v4
with:
name: playwright-traces-${{ inputs.runs-on }}-hg-${{ inputs.hg-version }}
Expand Down
1 change: 1 addition & 0 deletions backend/LexBoxApi/Services/EmailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ private async Task SendInvitationEmail(
var httpContext = httpContextAccessor.HttpContext;
ArgumentNullException.ThrowIfNull(httpContext);

//using GetPathByAction so the path is relative
var returnTo = _linkGenerator.GetPathByAction(httpContext,
nameof(Controllers.UserController.HandleInviteLink),
"User");
Expand Down
4 changes: 3 additions & 1 deletion deployment/gha/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ images:
- name: ghcr.io/sillsdev/lexbox-ui
newTag: develop
- name: ghcr.io/sillsdev/lexbox-hgweb
newTag: latest
newTag: develop
- name: ghcr.io/sillsdev/lexbox-fw-headless
newTag: develop
3 changes: 3 additions & 0 deletions deployment/gha/lexbox.patch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ spec:
spec:
containers:
- name: lexbox-api
env:
- name: Email__BaseUrl
value: "http://localhost:6579"
volumeMounts:
- mountPath: /frontend
name: gql-schema
Expand Down
16 changes: 0 additions & 16 deletions frontend/tests/envVars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ export const serverHostname = process.env.TEST_SERVER_HOSTNAME ?? 'localhost';
export const isDev = process.env.NODE_ENV === 'development' || serverHostname.startsWith('localhost');
export const httpScheme = isDev ? 'http://' : 'https://';
export const serverBaseUrl = `${httpScheme}${serverHostname}`;
export const standardHgHostname = process.env.TEST_STANDARD_HG_HOSTNAME ?? 'hg.localhost';
export const resumableHgHostname = process.env.TEST_RESUMABLE_HG_HOSTNAME ?? 'resumable.localhost';
export const resumableBaseUrl = `${httpScheme}${resumableHgHostname}`;
export const projectCode = process.env.TEST_PROJECT_CODE ?? 'sena-3';
export const defaultPassword = process.env.TEST_DEFAULT_PASSWORD ?? 'pass';
export const authCookieName = '.LexBoxAuth';
export const invalidJwt = 'eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiQWRtaW4iLCJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkphdmFJblVzZSIsImV4cCI6MTY5OTM0ODY2NywiaWF0IjoxNjk5MzQ4NjY3fQ.f8N63gcD_iv-E_x0ERhJwARaBKnZnORaZGe0N2J0VGM';
Expand All @@ -14,15 +10,3 @@ export const TEST_TIMEOUT = 40_000;
export const TEST_TIMEOUT_2X = TEST_TIMEOUT * 2;
export const ACTION_TIMEOUT = 5_000;
export const EXPECT_TIMEOUT = 5_000;

export enum HgProtocol {
Hgweb,
Resumable,
}

export function getTestHostName(protocol: HgProtocol) : string {
switch (protocol) {
case HgProtocol.Hgweb: return standardHgHostname;
case HgProtocol.Resumable: return resumableHgHostname;
}
}
179 changes: 91 additions & 88 deletions frontend/tests/viewerPage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,92 +6,95 @@ import {expect} from '@playwright/test';
import {loginAs} from './utils/authHelpers';
import {test} from './fixtures';

test('navigate to viewer', async ({ page }) => {
// Step 1: Login
await loginAs(page.request, 'editor', testEnv.defaultPassword);
const userDashboard = await new UserDashboardPage(page).goto();

// Step 2: Click through to viewer
const projectPage = await userDashboard.openProject('Sena 3', 'sena-3');
const viewerPage = await projectPage.clickBrowseInViewer();
await viewerPage.dismissAboutDialog();
});

test('find entry', async ({ page }) => {
// Step 1: Login to viewer
await loginAs(page.request, 'editor', testEnv.defaultPassword);
const viewerPage = await new ViewerPage(page, 'Sena 3', 'sena-3').goto();
await viewerPage.dismissAboutDialog();

// Step 2: Use search bar to go to entry "shrew"
await viewerPage.search('animal');
await expect(viewerPage.searchResults).toHaveCount(5);
await viewerPage.clickSearchResult('shrew');

async function verifyViewerState(): Promise<void> {
// Step 3: Check that we are on the shrew entry
await expect(viewerPage.entryDictionaryPreview).toContainText('shrew');
await expect(viewerPage.page.locator('.field:has-text("Citation form") input:visible'))
.toHaveValue('nkhwizi');

// Step 4: Verify entry list filter and order
await expect(viewerPage.entryListItems).toHaveCount(7);
await expect(viewerPage.entryListItems.nth(0)).toContainText('cifuwo');
await expect(viewerPage.entryListItems.nth(3)).toContainText('shrew');
await expect(viewerPage.entryListItems.nth(6)).toContainText('nyama');
}

await verifyViewerState();
await viewerPage.page.reload();
await viewerPage.waitFor();
await verifyViewerState();
});

test('entry details', async ({ page }) => {
// Step 1: Login to viewer at entry "thembe"
await loginAs(page.request, 'editor', testEnv.defaultPassword);
const viewerPage = await new ViewerPage(page, 'Sena 3', 'sena-3')
.goto({ urlEnd: '?entryId=49cc9257-90c7-4fe0-a9e0-2c8d72aa5e2b&search=animal' });
await viewerPage.dismissAboutDialog();

// Step 2: verify entry details
// -- Dictionary Preview
const expectPreview = expect(viewerPage.entryDictionaryPreview);
await expectPreview.toContainText('nthembe');
await expectPreview.toContainText(' Nome ');
await expectPreview.toContainText(' Eng ');
await expectPreview.toContainText('animal skin alone, after it is taken off the body');
await expectPreview.toContainText(' Por ');
await expectPreview.toContainText('pele de animal');

// -- Entry fields
const lexeme = viewerPage.page.locator('.field:has-text("Lexeme") .ws-field-wrapper:visible');
await expect(lexeme.getByRole('textbox')).toHaveValue('thembe');
await expect(lexeme.getByLabel('Sen', { exact: true })).toHaveValue('thembe');

await expect(viewerPage.page.locator('.field:has-text("Citation form") input:visible')).toHaveValue('nthembe');

// -- Sense
await expect(viewerPage.page.locator('[id^="sense"]')).toHaveCount(1);
await expect(viewerPage.page.locator(':text("gloss")')).toHaveCount(1);

const gloss = viewerPage.page.locator('.field:has-text("Gloss") .ws-field-wrapper:visible');
await expect(gloss).toHaveCount(2);
await expect(gloss.getByLabel('Eng')).toHaveValue('animal skin');
await expect(gloss.getByLabel('Por')).toHaveValue('pele de animal');

const definition = viewerPage.page.locator('.field:has-text("Definition") .ws-field-wrapper:visible');
await expect(definition).toHaveCount(1);
await expect(definition.getByLabel('Eng')).toHaveValue('animal skin alone, after it is taken off the body');

const grammaticalInfo = viewerPage.page.locator('.field:has-text("Grammatical info") input:visible');
await expect(grammaticalInfo).toHaveValue('Nome');

const semanticDomains = viewerPage.page.locator('.field:has-text("Semantic domain") input:visible');
await expect(semanticDomains).toHaveValue('1.6.2 Parts of an animal');

// -- Example
await expect(viewerPage.page.locator('[id^="example"]')).toHaveCount(1);
const reference = viewerPage.page.locator('.field:has-text("Reference") input');
await expect(reference).toHaveValue('Wordlist');
test.describe('Viewer Page', () => {

test('navigate to viewer', async ({page}) => {
// Step 1: Login
await loginAs(page.request, 'editor', testEnv.defaultPassword);
const userDashboard = await new UserDashboardPage(page).goto();

// Step 2: Click through to viewer
const projectPage = await userDashboard.openProject('Sena 3', 'sena-3');
const viewerPage = await projectPage.clickBrowseInViewer();
await viewerPage.dismissAboutDialog();
});

test('find entry', async ({page}) => {
// Step 1: Login to viewer
await loginAs(page.request, 'editor', testEnv.defaultPassword);
const viewerPage = await new ViewerPage(page, 'Sena 3', 'sena-3').goto();
await viewerPage.dismissAboutDialog();

// Step 2: Use search bar to go to entry "shrew"
await viewerPage.search('animal');
await expect(viewerPage.searchResults).toHaveCount(5);
await viewerPage.clickSearchResult('shrew');

async function verifyViewerState(): Promise<void> {
// Step 3: Check that we are on the shrew entry
await expect(viewerPage.entryDictionaryPreview).toContainText('shrew');
await expect(viewerPage.page.locator('.field:has-text("Citation form") input:visible'))
.toHaveValue('nkhwizi');

// Step 4: Verify entry list filter and order
await expect(viewerPage.entryListItems).toHaveCount(7);
await expect(viewerPage.entryListItems.nth(0)).toContainText('cifuwo');
await expect(viewerPage.entryListItems.nth(3)).toContainText('shrew');
await expect(viewerPage.entryListItems.nth(6)).toContainText('nyama');
}

await verifyViewerState();
await viewerPage.page.reload();
await viewerPage.waitFor();
await verifyViewerState();
});

test('entry details', async ({page}) => {
// Step 1: Login to viewer at entry "thembe"
await loginAs(page.request, 'editor', testEnv.defaultPassword);
const viewerPage = await new ViewerPage(page, 'Sena 3', 'sena-3')
.goto({urlEnd: '?entryId=49cc9257-90c7-4fe0-a9e0-2c8d72aa5e2b&search=animal'});
await viewerPage.dismissAboutDialog();

// Step 2: verify entry details
// -- Dictionary Preview
const expectPreview = expect(viewerPage.entryDictionaryPreview);
await expectPreview.toContainText('nthembe');
await expectPreview.toContainText(' Nome ');
await expectPreview.toContainText(' Eng ');
await expectPreview.toContainText('animal skin alone, after it is taken off the body');
await expectPreview.toContainText(' Por ');
await expectPreview.toContainText('pele de animal');

// -- Entry fields
const lexeme = viewerPage.page.locator('.field:has-text("Lexeme") .ws-field-wrapper:visible');
await expect(lexeme.getByRole('textbox')).toHaveValue('thembe');
await expect(lexeme.getByLabel('Sen', {exact: true})).toHaveValue('thembe');

await expect(viewerPage.page.locator('.field:has-text("Citation form") input:visible')).toHaveValue('nthembe');

// -- Sense
await expect(viewerPage.page.locator('[id^="sense"]')).toHaveCount(1);
await expect(viewerPage.page.locator(':text("gloss")')).toHaveCount(1);

const gloss = viewerPage.page.locator('.field:has-text("Gloss") .ws-field-wrapper:visible');
await expect(gloss).toHaveCount(2);
await expect(gloss.getByLabel('Eng')).toHaveValue('animal skin');
await expect(gloss.getByLabel('Por')).toHaveValue('pele de animal');

const definition = viewerPage.page.locator('.field:has-text("Definition") .ws-field-wrapper:visible');
await expect(definition).toHaveCount(1);
await expect(definition.getByLabel('Eng')).toHaveValue('animal skin alone, after it is taken off the body');

const grammaticalInfo = viewerPage.page.locator('.field:has-text("Grammatical info") input:visible');
await expect(grammaticalInfo).toHaveValue('Nome');

const semanticDomains = viewerPage.page.locator('.field:has-text("Semantic domain") input:visible');
await expect(semanticDomains).toHaveValue('1.6.2 Parts of an animal');

// -- Example
await expect(viewerPage.page.locator('[id^="example"]')).toHaveCount(1);
const reference = viewerPage.page.locator('.field:has-text("Reference") input');
await expect(reference).toHaveValue('Wordlist');
});
});

0 comments on commit c23a0bc

Please sign in to comment.