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
14 changes: 10 additions & 4 deletions .github/workflows/canister-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
II_FETCH_ROOT_KEY: 1
II_DUMMY_CAPTCHA: 1
II_DUMMY_AUTH: 0
II_DEV_CSP: 0
II_DEV_CSP: 1

# Everything disabled, used by third party developers who only care
# about the login flow
Expand Down Expand Up @@ -582,14 +582,15 @@ jobs:
needs: [cached-build, test-app-build]
strategy:
matrix:
browser: ["chrome", "safari"]
device: ["desktop", "mobile"]
shard: ["1_3", "2_3", "3_3"]
# Make sure that one failing test does not cancel all other matrix jobs
fail-fast: false

env:
# Suffix used for tagging artifacts
artifact_suffix: next-${{ matrix.device }}-${{ matrix.shard }}
artifact_suffix: next-${{ matrix.browser }}-${{ matrix.device }}-${{ matrix.shard }}
# OpenID configuration for provider in /src/test_openid_provider
openid_name: Test OpenID
openid_logo: |
Expand All @@ -615,7 +616,12 @@ jobs:
run: npm ci --no-audit --no-fund

- name: Install Playwright Browsers
run: npx playwright install chromium
run: |
if [ "${{ matrix.browser }}" = "chrome" ]; then
npx playwright install --with-deps chromium
else
npx playwright install --with-deps webkit
fi

- uses: dfinity/setup-dfx@e50c04f104ee4285ec010f10609483cf41e4d365

Expand Down Expand Up @@ -666,7 +672,7 @@ jobs:
echo "dev_server_pid=$dev_server_pid" >> "$GITHUB_OUTPUT"

- run: |
npx playwright test --project ${{ matrix.device }} --workers 1 --shard=$(tr <<<'${{ matrix.shard }}' -s _ /)
npx playwright test --project ${{ matrix.browser }}-${{ matrix.device }} --workers 1 --shard=$(tr <<<'${{ matrix.shard }}' -s _ /)

- name: Stop dfx
if: ${{ always() }}
Expand Down
16 changes: 16 additions & 0 deletions HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,22 @@ npx playwright test --ui
> II_CAPTCHA=enabled npm run test:e2e
> ```

**Writing new Playwright E2E tests:**

When creating new E2E Playwright tests, always import `test` and `expect` from the custom fixtures file:

```typescript
// ✅ Correct
import { test, expect } from "./fixtures";
// or from subdirectories:
import { test, expect } from "../fixtures";

// ❌ Wrong - will fail ESLint
import { test, expect } from "@playwright/test";
```

The custom fixtures provide automatic host routing for Safari/WebKit tests, which don't support `--host-resolver-rules`. ESLint will enforce this pattern and show an error if you try to import directly from `@playwright/test`.

We autoformat our code using `prettier`. Running `npm run format` formats all files in the frontend.
If you open a PR that isn't formatted according to `prettier`, CI will automatically add a formatting commit to your PR.

Expand Down
18 changes: 18 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,22 @@ export default ts.config(
{
ignores: ["src/frontend/src/lib/generated/*", "src/showcase/.astro/*"],
},
{
files: ["src/frontend/tests/e2e-playwright/**/*.spec.ts"],
rules: {
"no-restricted-imports": [
"error",
{
paths: [
{
name: "@playwright/test",
importNames: ["test", "expect"],
message:
"Import 'test' and 'expect' from './fixtures' or '../fixtures' instead to use custom test fixtures with host routing for Safari/WebKit.",
},
],
},
],
},
},
);
26 changes: 24 additions & 2 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,17 @@ export default defineConfig({

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

/* Ignore HTTPS errors, which is needed for the self-signed certificate. */
// It doesn't seem to fix it
// ignoreHTTPSErrors: true,
},
timeout: 60000,

/* Configure projects for major browsers */
projects: [
{
name: "desktop",
name: "chrome-desktop",
use: {
...devices["Desktop Chrome"],
launchOptions: {
Expand All @@ -48,7 +52,7 @@ export default defineConfig({
},
},
{
name: "mobile",
name: "chrome-mobile",
use: {
...devices["Pixel 5"],
launchOptions: {
Expand All @@ -59,5 +63,23 @@ export default defineConfig({
},
},
},
{
name: "safari-desktop",
use: {
...devices["Desktop Safari"],
launchOptions: {
args: ["--ignore-certificate-errors"],
},
},
},
{
name: "safari-mobile",
use: {
...devices["iPhone 12"],
launchOptions: {
args: ["--ignore-certificate-errors"],
},
},
},
],
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../fixtures";
import { authorize, createIdentity, dummyAuth } from "../utils";

test("Create and authorize with additional account", async ({ page }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect } from "@playwright/test";
import { test, expect } from "../fixtures";
import {
dummyAuth,
II_URL,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../fixtures";
import {
authorize,
authorizeWithUrl,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../fixtures";
import { dummyAuth, II_URL, TEST_APP_URL } from "../utils";

test("Delegation maxTimeToLive: 1 min", async ({ page }) => {
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/tests/e2e-playwright/authorize/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../fixtures";
import {
authorize,
authorizeWithUrl,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../fixtures";
import type Protocol from "devtools-protocol";
import {
addCredentialToVirtualAuthenticator,
Expand All @@ -15,6 +15,11 @@ import { isNullish } from "@dfinity/utils";
const TEST_USER_NAME = "Test User";

test.describe("Migration from an app", () => {
test.skip(
({ browserName }) => browserName === "webkit",
"Migration test not supported on Safari because it uses virtual authenticators which are not supported.",
);

test("User can migrate a legacy identity", async ({ page }) => {
const auth = dummyAuth();
let credential: Protocol.WebAuthn.Credential | undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../fixtures";
import {
createNewIdentityInII,
dummyAuth,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../fixtures";
import {
addPasskeyCurrentDevice,
clearStorage,
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/tests/e2e-playwright/dashboard/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../fixtures";
import { clearStorage, createIdentity, dummyAuth, II_URL } from "../utils";

const TEST_USER_NAME = "Test User";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, Page, test } from "@playwright/test";
import { expect, test } from "../fixtures";
import type { Page } from "@playwright/test";
import {
addVirtualAuthenticator,
clearStorage,
Expand Down Expand Up @@ -38,6 +39,11 @@ const upgradeLegacyIdentity = async (
};

test.describe("Migration", () => {
test.skip(
({ browserName }) => browserName === "webkit",
"Migration test not supported on Safari because it uses virtual authenticators which are not supported.",
);

test("User can migrate a legacy identity", async ({ page }) => {
// Step 1: Create a legacy identity
await page.goto(LEGACY_II_URL);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../fixtures";
import {
clearStorage,
createNewIdentityInII,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../fixtures";
import {
clearStorage,
createNewIdentityInII,
Expand Down
68 changes: 68 additions & 0 deletions src/frontend/tests/e2e-playwright/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { test as base, expect } from "@playwright/test";

/**
* Custom test fixture that automatically applies host resolution routing
* to redirect all non-localhost requests to localhost:5173
*
* ⚠️ IMPORTANT: All E2E test files MUST import { test, expect } from this file
* instead of from '@playwright/test' to ensure proper host routing for Safari/WebKit.
*
* @example
* // ✅ Correct
* import { test, expect } from "./fixtures";
* // or from subdirectories:
* import { test, expect } from "../fixtures";
*
* // ❌ Wrong - will fail ESLint
* import { test, expect } from "@playwright/test";
*/
export const test = base.extend({
page: async ({ page }, use) => {
const browserName = page.context().browser()?.browserType().name();

// Safari doesn't support --host-resolver-rules, so we need page routing for Safari
// Chromium browsers will use host-resolver-rules which preserves Host headers better
if (browserName === "webkit") {
await page.context().route("**/*", (route) => {
// Should map the config in `vite.config.ts`
const hostToCanisterName: Record<string, string> = {
["id.ai"]: "internet_identity",
["identity.ic0.app"]: "internet_identity",
["identity.internetcomputer.org"]: "internet_identity",
["nice-name.com"]: "test_app",
};

const req = route.request();
const urlStr = req.url();

let url: URL;
try {
url = new URL(urlStr);
} catch {
return route.continue();
}

if (url.hostname.includes("localhost")) {
return route.continue();
}

const canister_name = hostToCanisterName[url.hostname];
if (canister_name === undefined) {
return route.continue();
}
const newUrl = `https://${canister_name}.localhost:5173${url.pathname}${url.search}`;
return route.continue({
url: newUrl,
// The vite server uses the Host header to determine where the redirect the request.
headers: { ...req.headers(), Host: url.hostname },
});
});
}

// Use the page with routing applied
await use(page);
},
});

// Re-export expect for convenience
export { expect };
2 changes: 1 addition & 1 deletion src/frontend/tests/e2e-playwright/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "./fixtures";
import { clearStorage, createIdentity, dummyAuth, II_URL } from "./utils";

// This is chosen on purpose to exhibit a JWT token that is encoded in base64url but cannot
Expand Down
21 changes: 15 additions & 6 deletions src/internet_identity/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,19 @@ fn content_security_policy_header(
};

let connect_src = "'self' https:";
let script_src = format!("{strict_dynamic} 'unsafe-inline' 'unsafe-eval' https:");
let style_src = "'self' 'unsafe-inline'";
let img_src = "'self' data: https://*.googleusercontent.com";

// Allow connecting via http for development purposes
// Allow connecting via http and localhost (including subdomains) for development purposes
#[cfg(feature = "dev_csp")]
let connect_src = format!("{connect_src} http:");
let connect_src = format!("{connect_src} http: http://localhost:* http://*.localhost:*");
#[cfg(feature = "dev_csp")]
let script_src = format!("{script_src} http: http://localhost:* http://*.localhost:*");
#[cfg(feature = "dev_csp")]
let style_src = format!("{style_src} http: http://localhost:* http://*.localhost:*");
#[cfg(feature = "dev_csp")]
let img_src = format!("{img_src} http: http://localhost:* http://*.localhost:*");

// Allow related origins to embed one another for cross-domain WebAuthn
let frame_src = maybe_related_origins
Expand All @@ -273,12 +282,12 @@ fn content_security_policy_header(
let csp = format!(
"default-src 'none';\
connect-src {connect_src};\
img-src 'self' data: https://*.googleusercontent.com;\
script-src {strict_dynamic} 'unsafe-inline' 'unsafe-eval' https:;\
img-src {img_src};\
script-src {script_src};\
base-uri 'none';\
form-action 'none';\
style-src 'self' 'unsafe-inline';\
style-src-elem 'self' 'unsafe-inline';\
style-src {style_src};\
style-src-elem {style_src};\
font-src 'self';\
frame-ancestors {frame_src};\
frame-src {frame_src};"
Expand Down
Loading