Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run playwright tests in our gha integration test workflow #1132

Merged
merged 22 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
bd8d635
run playwright tests after S&R tests in the gha k8s instance
hahn-kev Oct 17, 2024
d1a8738
ensure maildev is forwarded properly
hahn-kev Oct 17, 2024
de66267
remove comment
hahn-kev Oct 17, 2024
c168bb5
install dependancies during playwright setup
hahn-kev Oct 28, 2024
8689c8c
fix email base url for gha environment
hahn-kev Oct 29, 2024
d05abe2
fix regex used for AddMemberModal tests
hahn-kev Oct 29, 2024
b4f78d2
ensure return to path used in LoginRedirect is relative for invite em…
hahn-kev Oct 29, 2024
0c08bbe
Merge branch 'develop' into chore/playwright-in-gha-k8s
hahn-kev Nov 6, 2024
2935240
Merge remote-tracking branch 'origin/develop' into chore/playwright-i…
myieye Nov 13, 2024
0c52d88
remove self hosted step and unused inputs
hahn-kev Oct 17, 2024
022b1ca
Merge branch 'develop' into chore/playwright-in-gha-k8s
hahn-kev Nov 14, 2024
8f25bd0
fix url absolute check, ensure task setup only modifies docker-desktop
hahn-kev Nov 15, 2024
8d408b2
Exclude viewer page playwright tests in GHA
myieye Nov 15, 2024
a65d493
Run dotnet and playwright tests in parallel
myieye Nov 15, 2024
dbb46f0
Pass k8s-context to follow-up jobs and forward ports there
myieye Nov 15, 2024
8d7eb20
Revert "Pass k8s-context to follow-up jobs and forward ports there"
myieye Nov 18, 2024
39eac49
Revert "Run dotnet and playwright tests in parallel"
myieye Nov 18, 2024
f839ef4
Run Playwright tests even if dotnet tests fail
myieye Nov 18, 2024
fefddfd
Merge branch 'develop' into chore/playwright-in-gha-k8s
myieye Nov 18, 2024
1b96715
use develop image tag for lexbox-hgweb and fw-headless in gha integra…
hahn-kev Nov 19, 2024
ce96d52
remove unused environment variables from frontend tests
hahn-kev Nov 19, 2024
caf0328
pull playwright tests into a reusable action and call them from both …
hahn-kev Nov 19, 2024
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
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');
});
});
Loading