mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
E2E runner updates (#64505)
* Add a helper for getting env vars we expect to be defined * Enable strict mode in TypeScript, add tsx for running browser * Add custom script for running browse/codegen modes with webauthn setup * Normalize test file paths to the e2e directory * Wire up the --teleport-log-level flag * Add @avatus, @ravicious, @ryanclark as codeowners of e2e deps * Keep e2e tsconfig.json isolated, will run type-check in CI * Reset changes to root tsconfig.json * Wrap `import.meta.url` in `fileURLToPath`
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
# Owners for JS dependency updates.
|
||||
/pnpm-lock.yaml @avatus @gzdunek @ravicious @zmb3 @r0mant
|
||||
/web/packages/teleterm/package.json @gzdunek @ravicious
|
||||
/e2e/pnpm-lock.yaml @ryanclark @avatus @ravicious
|
||||
|
||||
# Owners for Go dependency updates.
|
||||
/go.mod @russjones @r0mant @zmb3 @rosstimothy
|
||||
|
||||
@@ -6,7 +6,7 @@ teleport:
|
||||
data_dir: {{ .DataDir }}
|
||||
log:
|
||||
output: stderr
|
||||
severity: INFO
|
||||
severity: {{ .LogLevel }}
|
||||
format:
|
||||
output: text
|
||||
auth_server: localhost:{{ .AuthServerPort }}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Teleport
|
||||
* Copyright (C) 2026 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// This file provides helper functions to read required environment variables for the E2E tests.
|
||||
|
||||
function required(name: string) {
|
||||
const value = process.env[name];
|
||||
|
||||
if (!value) {
|
||||
throw new Error(`required environment variable ${name} is not set`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export const password = required('E2E_PASSWORD');
|
||||
export const webauthnPrivateKey = required('E2E_WEBAUTHN_PRIVATE_KEY');
|
||||
export const webauthnCredentialId = required('E2E_WEBAUTHN_CREDENTIAL_ID');
|
||||
export const inviteUrl = required('E2E_INVITE_URL');
|
||||
@@ -18,13 +18,14 @@
|
||||
|
||||
import { type Page } from '@playwright/test';
|
||||
|
||||
import { password as e2ePassword } from './env';
|
||||
import { expect } from './test';
|
||||
import { mockWebAuthn } from './webauthn';
|
||||
|
||||
export async function login(
|
||||
page: Page,
|
||||
username = 'bob',
|
||||
password = process.env.E2E_PASSWORD
|
||||
password = e2ePassword
|
||||
) {
|
||||
await page.addInitScript(() =>
|
||||
localStorage.setItem('grv_teleport_license_acknowledged', 'true')
|
||||
|
||||
@@ -23,12 +23,13 @@ import { expect } from '../test';
|
||||
const TERMINAL_TIMEOUT = 10_000;
|
||||
|
||||
export class TerminalPage {
|
||||
private readonly input = this.page.getByRole('textbox', {
|
||||
name: 'Terminal input',
|
||||
});
|
||||
private readonly terminal = this.page.getByTestId('terminal');
|
||||
private readonly input;
|
||||
private readonly terminal;
|
||||
|
||||
constructor(private page: Page) {}
|
||||
constructor(private page: Page) {
|
||||
this.input = page.getByRole('textbox', { name: 'Terminal input' });
|
||||
this.terminal = page.getByTestId('terminal');
|
||||
}
|
||||
|
||||
async waitForReady() {
|
||||
await expect(this.input).toBeVisible({ timeout: TERMINAL_TIMEOUT });
|
||||
|
||||
@@ -18,12 +18,13 @@
|
||||
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { inviteUrl } from './env';
|
||||
import { mockWebAuthn } from './webauthn';
|
||||
|
||||
export async function signup(page: Page) {
|
||||
await mockWebAuthn(page);
|
||||
|
||||
await page.goto(process.env.E2E_INVITE_URL);
|
||||
await page.goto(inviteUrl);
|
||||
|
||||
await page.getByRole('button', { name: 'Get started' }).click();
|
||||
await page.getByRole('textbox', { name: 'Password', exact: true }).click();
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
const privateKeyBase64 = process.env.E2E_WEBAUTHN_PRIVATE_KEY;
|
||||
const credentialIdBase64 = process.env.E2E_WEBAUTHN_CREDENTIAL_ID;
|
||||
import { webauthnCredentialId, webauthnPrivateKey } from './env';
|
||||
|
||||
// mockWebAuthn sets up a virtual webauthn authenticator on the page.
|
||||
export async function mockWebAuthn(page: Page) {
|
||||
@@ -42,10 +41,10 @@ export async function mockWebAuthn(page: Page) {
|
||||
await cdpSession.send('WebAuthn.addCredential', {
|
||||
authenticatorId,
|
||||
credential: {
|
||||
credentialId: credentialIdBase64,
|
||||
credentialId: webauthnCredentialId,
|
||||
isResidentCredential: false,
|
||||
rpId: 'localhost',
|
||||
privateKey: privateKeyBase64,
|
||||
privateKey: webauthnPrivateKey,
|
||||
signCount: 0,
|
||||
},
|
||||
});
|
||||
|
||||
+4
-1
@@ -1,13 +1,16 @@
|
||||
{
|
||||
"name": "@gravitational/e2e",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "Playwright tests for e2e testing.",
|
||||
"scripts": {
|
||||
"test": "./run.sh",
|
||||
"type-check": "tsc --noEmit",
|
||||
"show-report": "playwright show-report --port 0"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.55.1"
|
||||
"@playwright/test": "^1.55.1",
|
||||
"tsx": "^4.21.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+303
@@ -11,19 +11,191 @@ importers:
|
||||
'@playwright/test':
|
||||
specifier: ^1.55.1
|
||||
version: 1.56.1
|
||||
tsx:
|
||||
specifier: ^4.21.0
|
||||
version: 4.21.0
|
||||
|
||||
packages:
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.27.3':
|
||||
resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.27.3':
|
||||
resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.27.3':
|
||||
resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.27.3':
|
||||
resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.27.3':
|
||||
resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.27.3':
|
||||
resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.3':
|
||||
resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.3':
|
||||
resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.3':
|
||||
resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.27.3':
|
||||
resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.27.3':
|
||||
resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.27.3':
|
||||
resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.27.3':
|
||||
resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.27.3':
|
||||
resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@playwright/test@1.56.1':
|
||||
resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
esbuild@0.27.3:
|
||||
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
get-tsconfig@4.13.6:
|
||||
resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==}
|
||||
|
||||
playwright-core@1.56.1:
|
||||
resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -34,15 +206,137 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
resolve-pkg-maps@1.0.0:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
|
||||
tsx@4.21.0:
|
||||
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@playwright/test@1.56.1':
|
||||
dependencies:
|
||||
playwright: 1.56.1
|
||||
|
||||
esbuild@0.27.3:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.3
|
||||
'@esbuild/android-arm': 0.27.3
|
||||
'@esbuild/android-arm64': 0.27.3
|
||||
'@esbuild/android-x64': 0.27.3
|
||||
'@esbuild/darwin-arm64': 0.27.3
|
||||
'@esbuild/darwin-x64': 0.27.3
|
||||
'@esbuild/freebsd-arm64': 0.27.3
|
||||
'@esbuild/freebsd-x64': 0.27.3
|
||||
'@esbuild/linux-arm': 0.27.3
|
||||
'@esbuild/linux-arm64': 0.27.3
|
||||
'@esbuild/linux-ia32': 0.27.3
|
||||
'@esbuild/linux-loong64': 0.27.3
|
||||
'@esbuild/linux-mips64el': 0.27.3
|
||||
'@esbuild/linux-ppc64': 0.27.3
|
||||
'@esbuild/linux-riscv64': 0.27.3
|
||||
'@esbuild/linux-s390x': 0.27.3
|
||||
'@esbuild/linux-x64': 0.27.3
|
||||
'@esbuild/netbsd-arm64': 0.27.3
|
||||
'@esbuild/netbsd-x64': 0.27.3
|
||||
'@esbuild/openbsd-arm64': 0.27.3
|
||||
'@esbuild/openbsd-x64': 0.27.3
|
||||
'@esbuild/openharmony-arm64': 0.27.3
|
||||
'@esbuild/sunos-x64': 0.27.3
|
||||
'@esbuild/win32-arm64': 0.27.3
|
||||
'@esbuild/win32-ia32': 0.27.3
|
||||
'@esbuild/win32-x64': 0.27.3
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
get-tsconfig@4.13.6:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
playwright-core@1.56.1: {}
|
||||
|
||||
playwright@1.56.1:
|
||||
@@ -50,3 +344,12 @@ snapshots:
|
||||
playwright-core: 1.56.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
tsx@4.21.0:
|
||||
dependencies:
|
||||
esbuild: 0.27.3
|
||||
get-tsconfig: 4.13.6
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
+49
-1
@@ -24,6 +24,8 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var sshNode = registerFixture("ssh-node", "start and connect a Teleport SSH node, runs in Docker")
|
||||
@@ -43,6 +45,8 @@ type e2eFlags struct {
|
||||
testFiles []string
|
||||
}
|
||||
|
||||
var validTeleportLogLevels = []string{"DEBUG", "INFO", "WARN", "ERROR"}
|
||||
|
||||
func parseFlags(repoRoot string) (*e2eFlags, runMode, error) {
|
||||
var f e2eFlags
|
||||
|
||||
@@ -84,13 +88,57 @@ func parseFlags(repoRoot string) (*e2eFlags, runMode, error) {
|
||||
enableAllFixtures()
|
||||
}
|
||||
|
||||
f.testFiles = flag.Args()
|
||||
f.teleportLogLevel = strings.ToUpper(f.teleportLogLevel)
|
||||
if !slices.Contains(validTeleportLogLevels, f.teleportLogLevel) {
|
||||
return nil, 0, fmt.Errorf("invalid --teleport-log-level %q, must be one of: %s", f.teleportLogLevel, strings.Join(validTeleportLogLevels, ", "))
|
||||
}
|
||||
|
||||
e2eDir := filepath.Join(repoRoot, "e2e")
|
||||
|
||||
var err error
|
||||
f.testFiles, err = normalizeTestFiles(e2eDir, flag.Args())
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
mode, err := modes.resolve()
|
||||
|
||||
return &f, mode, err
|
||||
}
|
||||
|
||||
func normalizeTestFiles(e2eDir string, args []string) ([]string, error) {
|
||||
if len(args) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
callerDir := os.Getenv("E2E_CALLER_DIR")
|
||||
if callerDir == "" {
|
||||
var err error
|
||||
callerDir, err = os.Getwd()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting current working directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
normalized := make([]string, 0, len(args))
|
||||
for _, arg := range args {
|
||||
abs := arg
|
||||
if !filepath.IsAbs(abs) {
|
||||
abs = filepath.Join(callerDir, abs)
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(e2eDir, abs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("making %q relative to e2e dir: %w", arg, err)
|
||||
}
|
||||
|
||||
normalized = append(normalized, rel)
|
||||
}
|
||||
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func stringFlagWithEnv(fs *flag.FlagSet, p *string, name, env, fallback, usage string) {
|
||||
if v := os.Getenv(env); v != "" {
|
||||
fallback = v
|
||||
|
||||
@@ -44,6 +44,26 @@ func (p *playwrightRunner) startURL() string {
|
||||
return fmt.Sprintf("https://localhost:%d/web", p.config.proxyPort)
|
||||
}
|
||||
|
||||
// callerRelativePaths returns paths relative to the caller's working directory
|
||||
// so that logged paths are cmd+clickable in terminals.
|
||||
func (p *playwrightRunner) callerRelativePaths(paths []string) []string {
|
||||
callerDir := os.Getenv("E2E_CALLER_DIR")
|
||||
if callerDir == "" {
|
||||
return paths
|
||||
}
|
||||
|
||||
out := make([]string, len(paths))
|
||||
for i, path := range paths {
|
||||
abs := filepath.Join(p.config.e2eDir, path)
|
||||
if rel, err := filepath.Rel(callerDir, abs); err == nil {
|
||||
out[i] = rel
|
||||
} else {
|
||||
out[i] = path
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (p *playwrightRunner) run(ctx context.Context, mode runMode) error {
|
||||
switch mode {
|
||||
case modeTest:
|
||||
@@ -78,7 +98,7 @@ func (p *playwrightRunner) test(ctx context.Context, debug bool) error {
|
||||
if len(p.config.testFiles) > 0 {
|
||||
args := append([]string{"exec", "playwright", "test"}, extraArgs...)
|
||||
args = append(args, p.config.testFiles...)
|
||||
slog.Info("running e2e tests", "files", p.config.testFiles)
|
||||
slog.Info("running e2e tests", "files", p.callerRelativePaths(p.config.testFiles))
|
||||
return p.pnpm(ctx, args, env)
|
||||
}
|
||||
|
||||
@@ -113,7 +133,8 @@ func (p *playwrightRunner) browse(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// openAuthenticated runs the setup project to generate auth state, then opens
|
||||
// Playwright in the given mode (codegen or open) with the saved session.
|
||||
// a Chromium browser with a virtual WebAuthn authenticator pre-loaded so that
|
||||
// MFA challenges resolve automatically.
|
||||
func (p *playwrightRunner) openAuthenticated(ctx context.Context, playwrightCmd string) error {
|
||||
env, err := p.startEnv(ctx)
|
||||
if err != nil {
|
||||
@@ -125,12 +146,11 @@ func (p *playwrightRunner) openAuthenticated(ctx context.Context, playwrightCmd
|
||||
return err
|
||||
}
|
||||
|
||||
slog.Info("opening playwright " + playwrightCmd + " (with auth)")
|
||||
slog.Info("opening playwright " + playwrightCmd + " (with auth and WebAuthn)")
|
||||
|
||||
return p.pnpm(ctx, []string{
|
||||
"exec", "playwright", playwrightCmd,
|
||||
"--load-storage=.auth/user.json",
|
||||
"--ignore-https-errors",
|
||||
"exec", "tsx", "scripts/open-with-webauthn.ts",
|
||||
playwrightCmd,
|
||||
p.startURL(),
|
||||
}, env)
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ type TeleportConfig struct {
|
||||
KeyFilePath string
|
||||
CertFilePath string
|
||||
LicenseFile string
|
||||
LogLevel string
|
||||
}
|
||||
|
||||
func generateTeleportConfig(templatePath string, config *e2eConfig) (string, error) {
|
||||
@@ -159,6 +160,7 @@ func generateTeleportConfig(templatePath string, config *e2eConfig) (string, err
|
||||
KeyFilePath: filepath.Join(config.certsDir, keyFileName),
|
||||
CertFilePath: filepath.Join(config.certsDir, certFileName),
|
||||
LicenseFile: config.licenseFile,
|
||||
LogLevel: config.teleportLogLevel,
|
||||
}
|
||||
|
||||
return renderTemplate(templatePath, teleportConfig)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Teleport
|
||||
* Copyright (C) 2026 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// open-with-webauthn launches a Chromium browser with a virtual WebAuthn
|
||||
// authenticator preloaded so that MFA challenges resolve automatically
|
||||
// in codegen and browse modes.
|
||||
//
|
||||
// Usage: pnpm exec tsx scripts/open-with-webauthn.ts <codegen|open> <url>
|
||||
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { chromium, type BrowserContext } from '@playwright/test';
|
||||
|
||||
import { mockWebAuthn } from '../helpers/webauthn';
|
||||
|
||||
const bold = (s: string) => `\x1b[1m${s}\x1b[22m`;
|
||||
const green = (s: string) => `\x1b[32m${s}\x1b[39m`;
|
||||
const cyan = (s: string) => `\x1b[36m${s}\x1b[39m`;
|
||||
const red = (s: string) => `\x1b[31m${s}\x1b[39m`;
|
||||
const dim = (s: string) => `\x1b[2m${s}\x1b[22m`;
|
||||
|
||||
function info(msg: string) {
|
||||
process.stdout.write(`${green('✓')} ${msg}\n`);
|
||||
}
|
||||
|
||||
function error(msg: string) {
|
||||
process.stderr.write(`${red('✗')} ${msg}\n`);
|
||||
}
|
||||
|
||||
const mode = process.argv[2] as 'codegen' | 'open';
|
||||
const startURL = process.argv[3];
|
||||
|
||||
if (!mode || !startURL) {
|
||||
error(
|
||||
'Usage: pnpm exec tsx scripts/open-with-webauthn.ts <codegen|open> <url>'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const e2eDir = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const storageStatePath = join(e2eDir, '.auth/user.json');
|
||||
|
||||
info(`launching Chromium ${dim(`(mode: ${mode})`)}`);
|
||||
|
||||
const browser = await chromium.launch({ headless: false });
|
||||
const context = await browser.newContext({
|
||||
storageState: storageStatePath,
|
||||
ignoreHTTPSErrors: true,
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
await mockWebAuthn(page);
|
||||
|
||||
info('virtual WebAuthn authenticator registered');
|
||||
|
||||
if (mode === 'codegen') {
|
||||
// _enableRecorder is the internal API that `playwright codegen` itself uses
|
||||
// to attach the code-generation inspector to a browser context.
|
||||
type ExtendedBrowserContext = BrowserContext & {
|
||||
_enableRecorder: (options: {
|
||||
language: string;
|
||||
mode: 'recording';
|
||||
}) => Promise<void>;
|
||||
};
|
||||
|
||||
await (context as ExtendedBrowserContext)._enableRecorder({
|
||||
language: 'javascript',
|
||||
mode: 'recording',
|
||||
});
|
||||
|
||||
info('Playwright recorder enabled');
|
||||
}
|
||||
|
||||
info(`navigating to ${cyan(bold(startURL))}`);
|
||||
|
||||
await page.goto(startURL);
|
||||
|
||||
// Exit when the user closes the last page or the browser disconnects.
|
||||
page.on('close', () => {
|
||||
if (context.pages().length === 0) {
|
||||
browser.close().finally(() => process.exit(0));
|
||||
}
|
||||
});
|
||||
browser.on('disconnected', () => process.exit(0));
|
||||
|
||||
// Block until exit.
|
||||
await new Promise(() => {});
|
||||
@@ -16,12 +16,13 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { join } from 'node:path';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { login } from '../helpers/login';
|
||||
import { test as setup } from '../helpers/test';
|
||||
|
||||
const authFile = join(__dirname, '../.auth/user.json');
|
||||
const authFile = join(dirname(fileURLToPath(import.meta.url)), '../.auth/user.json');
|
||||
|
||||
setup('authenticate', async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user