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

Browser automation test with Playwright #39

Merged
merged 5 commits into from
Nov 29, 2024
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
7 changes: 6 additions & 1 deletion .github/workflows/nightly-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ on:
workflow_dispatch:

jobs:
nightly-e2e:
# Cannot check the existence of secrets, so limiting to repository name to prevent all forks to run nightly.
# See: https://github.com/actions/runner/issues/520
if: ${{ github.repository == 'Stormbase/django-otp-webauthn' }}
uses: ./.github/workflows/playwright-tests.yml
nightly-test:
# Cannot check the existence of secrets, so limiting to repository name to prevent all forks to run nightly.
# See: https://github.com/actions/runner/issues/520
Expand Down Expand Up @@ -38,7 +43,7 @@ jobs:

- name: Compile translations
run: |
/bin/sh ./compile_translations.sh
/bin/sh ./scripts/compile_translations.sh

- name: Run 'future' tests
id: test
Expand Down
49 changes: 49 additions & 0 deletions .github/workflows/playwright-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Playwright Tests
on:
workflow_call:
workflow_dispatch:

env:
FORCE_COLOR: "1" # Make tools pretty.
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PIP_NO_PYTHON_VERSION_WARNING: "1"

jobs:
playwright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.13"
cache: "pip"
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Install additional system packages
run: |
sudo apt-get update -y
# Need `gettext` to compile translations
sudo apt-get install gettext
- name: Install dependencies
run: |
yarn install --frozen-lockfile
- name: Compile JavaScript
run: |
yarn build
- name: Install dependencies
run: |
python -m pip install .[testing,playwright]
- name: Compile translations
run: /bin/sh ./scripts/compile_translations.sh
- name: Run E2E tests
run: |
/bin/sh ./scripts/run_e2e_testsuite.sh
- name: Upload Playwright traces
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-traces
path: test-results/
retention-days: 3
2 changes: 1 addition & 1 deletion .github/workflows/publish-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:

- name: Compile translations
run: |
/bin/sh ./compile_translations.sh
/bin/sh ./scripts/compile_translations.sh

- name: Build package distributions
run: python -m build
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:

- name: Compile translations
run: |
/bin/sh ./compile_translations.sh
/bin/sh ./scripts/compile_translations.sh

- name: Build package distributions
run: python -m build
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
python -m pip install .[testing] tox coverage

- name: Compile translations
run: /bin/sh ./compile_translations.sh
run: /bin/sh ./scripts/compile_translations.sh

# This step runs only for jobs NOT in the include matrix
- name: Run tox targets for Python ${{ matrix.python }}
Expand All @@ -77,3 +77,7 @@ jobs:
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

e2e_tests:
name: End-to-End Tests
uses: ./.github/workflows/playwright-tests.yml
18 changes: 17 additions & 1 deletion client/src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { State, Config } from "./types";
import { State, Config, StatusEnum } from "./types";
import { getConfig } from "./utils";
import {
browserSupportsWebAuthn,
Expand Down Expand Up @@ -220,6 +220,7 @@ import {
);
await setPasskeyVerifyState({
buttonDisabled: true,
statusEnum: StatusEnum.BUSY,
buttonLabel: gettext("Verifying..."),
});

Expand All @@ -238,6 +239,7 @@ import {
buttonDisabled: false,
buttonLabel,
requestFocus: true,
statusEnum: StatusEnum.GET_OPTIONS_FAILED,
status: gettext(
"Verification failed. Could not retrieve parameters from the server.",
),
Expand Down Expand Up @@ -271,6 +273,7 @@ import {
buttonDisabled: false,
buttonLabel,
requestFocus: true,
statusEnum: StatusEnum.ABORTED,
status: gettext("Verification aborted."),
});
break;
Expand All @@ -279,6 +282,7 @@ import {
buttonDisabled: false,
buttonLabel,
requestFocus: true,
statusEnum: StatusEnum.NOT_ALLOWED_OR_ABORTED,
status: gettext("Verification canceled or not allowed."),
});
break;
Expand All @@ -287,6 +291,7 @@ import {
buttonDisabled: false,
buttonLabel,
requestFocus: true,
statusEnum: StatusEnum.UNKNOWN_ERROR,
status: gettext(
"Verification failed. An unknown error occurred.",
),
Expand All @@ -308,6 +313,7 @@ import {

await setPasskeyVerifyState({
buttonDisabled: true,
statusEnum: StatusEnum.BUSY,
buttonLabel: gettext("Finishing verification..."),
});

Expand Down Expand Up @@ -340,6 +346,7 @@ import {
buttonDisabled: false,
buttonLabel,
requestFocus: true,
statusEnum: StatusEnum.SERVER_ERROR,
status: gettext(
"Verification failed. An unknown server error occurred.",
),
Expand All @@ -366,6 +373,7 @@ import {
await setPasskeyVerifyState({
buttonDisabled: false,
buttonLabel,
statusEnum: StatusEnum.SERVER_ERROR,
requestFocus: true,
status: msg,
});
Expand All @@ -386,6 +394,7 @@ import {
await setPasskeyVerifyState({
buttonDisabled: false,
buttonLabel,
statusEnum: StatusEnum.SUCCESS,
status: gettext("Verification successful!"),
});

Expand All @@ -411,6 +420,7 @@ import {
await setPasskeyVerifyState({
buttonDisabled: false,
buttonLabel,
statusEnum: StatusEnum.SERVER_ERROR,
requestFocus: true,
status: msg,
});
Expand All @@ -432,6 +442,12 @@ import {
passkeyAuthButton.disabled = state.buttonDisabled;
passkeyAuthButton.textContent = state.buttonLabel;

if (state.statusEnum) {
passkeyStatusText.setAttribute("data-status-enum", state.statusEnum);
} else {
passkeyStatusText.removeAttribute("data-status-enum");
}

if (passkeyStatusText) {
// If there is a status message, we want to make sure screen readers
// announce it to the user for clarity as to what is happening.
Expand Down
19 changes: 18 additions & 1 deletion client/src/register.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { State, Config } from "./types";
import { State, Config, StatusEnum } from "./types";
import { getConfig } from "./utils";
import {
browserSupportsWebAuthn,
Expand Down Expand Up @@ -60,6 +60,7 @@ import {
buttonDisabled: false,
buttonLabel,
requestFocus: true,
statusEnum: StatusEnum.GET_OPTIONS_FAILED,
status: gettext(
"Registration failed. Unable to fetch registration options from server.",
),
Expand Down Expand Up @@ -89,6 +90,7 @@ import {
setPasskeyRegisterState({
buttonDisabled: false,
buttonLabel,
statusEnum: StatusEnum.ABORTED,
status: gettext("Registration aborted."),
requestFocus: true,
});
Expand All @@ -97,6 +99,7 @@ import {
setPasskeyRegisterState({
buttonDisabled: false,
buttonLabel,
statusEnum: StatusEnum.STATE_ERROR,
status: gettext(
"Registration failed. You most likely already have a Passkey registered for this site.",
),
Expand All @@ -107,6 +110,7 @@ import {
setPasskeyRegisterState({
buttonDisabled: false,
buttonLabel,
statusEnum: StatusEnum.NOT_ALLOWED_OR_ABORTED,
status: gettext("Registration aborted or not allowed."),
requestFocus: true,
});
Expand All @@ -115,6 +119,7 @@ import {
setPasskeyRegisterState({
buttonDisabled: false,
buttonLabel,
statusEnum: StatusEnum.SECURITY_ERROR,
status: gettext(
"Registration failed. A technical problem occurred that prevents you from registering a Passkey for this site.",
),
Expand All @@ -125,6 +130,7 @@ import {
setPasskeyRegisterState({
buttonDisabled: false,
buttonLabel,
statusEnum: StatusEnum.UNKNOWN_ERROR,
status: gettext(
"Registration failed. An unknown error occurred.",
),
Expand All @@ -146,6 +152,7 @@ import {

setPasskeyRegisterState({
buttonDisabled: true,
statusEnum: StatusEnum.BUSY,
buttonLabel: gettext("Finishing registration..."),
});

Expand All @@ -164,6 +171,7 @@ import {
setPasskeyRegisterState({
buttonDisabled: false,
buttonLabel,
statusEnum: StatusEnum.SERVER_ERROR,
status: gettext(
"Registration failed. The server was unable to verify this passkey.",
),
Expand All @@ -189,6 +197,7 @@ import {
setPasskeyRegisterState({
buttonDisabled: false,
buttonLabel,
statusEnum: StatusEnum.SERVER_ERROR,
status: gettext("Registration failed. A server error occurred."),
requestFocus: true,
});
Expand All @@ -211,6 +220,7 @@ import {
setPasskeyRegisterState({
buttonDisabled: false,
buttonLabel,
statusEnum: StatusEnum.SUCCESS,
status: gettext("Registration successful!"),
requestFocus: true,
});
Expand All @@ -230,6 +240,7 @@ import {
setPasskeyRegisterState({
buttonDisabled: false,
buttonLabel,
statusEnum: StatusEnum.SERVER_ERROR,
status: msg,
requestFocus: true,
});
Expand Down Expand Up @@ -258,6 +269,12 @@ import {
passkeyRegisterButton.disabled = state.buttonDisabled;
passkeyRegisterButton.textContent = state.buttonLabel;

if (state.statusEnum) {
passkeyStatusText.setAttribute("data-status-enum", state.statusEnum);
} else {
passkeyStatusText.removeAttribute("data-status-enum");
}

if (passkeyStatusText) {
if (state.status) {
// If there is a status message, we want to make sure screen readers
Expand Down
13 changes: 13 additions & 0 deletions client/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
export enum StatusEnum {
UNKNOWN_ERROR = "unknown-error",
STATE_ERROR = "state-error",
SECURITY_ERROR = "security-error",
GET_OPTIONS_FAILED = "get-options-failed",
ABORTED = "aborted",
NOT_ALLOWED_OR_ABORTED = "not-allowed-or-aborted",
SERVER_ERROR = "server-error",
SUCCESS = "success",
BUSY = "busy",
}

export type State = {
buttonDisabled: boolean;
buttonLabel: string;
/** Text to display in the status field. */
status?: string;
statusEnum?: StatusEnum;
/** Request the focus be returned to the button. */
requestFocus?: boolean;
};
Expand Down
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ testing = [
"pytest-mock>=3.14,<4",
"jsonschema ~= 4.23",
]
playwright = [
"playwright>=1.49,<2",
"pytest-playwright>=0.5.2,<1",
]

[project.urls]
# TODO: documentation link
Expand Down Expand Up @@ -88,8 +92,7 @@ include = [
"sandbox/",
"client/",
"tests/",
"update_translations.sh",
"compile_translations.sh",
"scripts/",
"README.md",
"LICENSE",
"CHANGELOG.md",
Expand Down Expand Up @@ -131,6 +134,7 @@ known-first-party = ["django_otp_webauthn", "sandbox", "tests"]
pythonpath = "sandbox"
DJANGO_SETTINGS_MODULE = "settings"
testpaths = "tests/"
norecursedirs = ["tests/e2e"]
addopts = "--reuse-db"

[tool.coverage.run]
Expand Down
Loading