diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue
index cbfa24053..07cf4b167 100644
--- a/frontend/src/views/LoginView.vue
+++ b/frontend/src/views/LoginView.vue
@@ -214,9 +214,9 @@ const onEnter = () => {
@@ -226,6 +226,7 @@ const onEnter = () => {
:disabled="isLoading"
@click="onEnter()"
v-if="loginStep !== LoginSteps.SignUpConfirm"
+ data-testid="login-continue-btn"
>
{{ t('label.continue') }}
@@ -235,6 +236,7 @@ const onEnter = () => {
:disabled="isLoading"
@click="router.push({name: 'home'})"
v-else
+ data-testid="login-close-btn"
>
{{ t('label.close') }}
diff --git a/test/e2e/.env.example b/test/e2e/.env.example
new file mode 100644
index 000000000..bc84db057
--- /dev/null
+++ b/test/e2e/.env.example
@@ -0,0 +1,5 @@
+# Appointment E2E Test Configuration
+
+# Production sign-in (FxA) credentials
+APPT_PROD_LOGIN_EMAIL=
+APPT_PROD_LOGIN_PWORD=
diff --git a/test/e2e/.gitignore b/test/e2e/.gitignore
index 68c5d18f0..d60ce5cc8 100644
--- a/test/e2e/.gitignore
+++ b/test/e2e/.gitignore
@@ -1,3 +1,4 @@
+.env
node_modules/
/test-results/
/playwright-report/
diff --git a/test/e2e/README.md b/test/e2e/README.md
index 91fc11b9e..608c0081b 100644
--- a/test/e2e/README.md
+++ b/test/e2e/README.md
@@ -5,6 +5,7 @@ Guide for running the E2E tests.
## Installation
todo
+include npm install . etc
```bash
todo
diff --git a/test/e2e/const/constants.ts b/test/e2e/const/constants.ts
index 10e17924d..5cc6f9026 100644
--- a/test/e2e/const/constants.ts
+++ b/test/e2e/const/constants.ts
@@ -3,3 +3,8 @@ export const APPT_PROD_URL = 'https://appointment.day/';
// page titles
export const APPT_PAGE_TITLE = 'Thunderbird Appointment';
+export const FXA_PAGE_TITLE = 'Mozilla accounts';
+
+// production sign-in credentials
+export const PROD_LOGIN_EMAIL = process.env.APPT_PROD_LOGIN_EMAIL;
+export const PROD_LOGIN_PWORD = process.env.APPT_PROD_LOGIN_PWORD;
diff --git a/test/e2e/package-lock.json b/test/e2e/package-lock.json
index 01afcda67..c0a574c7f 100644
--- a/test/e2e/package-lock.json
+++ b/test/e2e/package-lock.json
@@ -10,7 +10,8 @@
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.49.0",
- "@types/node": "^22.10.1"
+ "@types/node": "^22.10.1",
+ "dotenv": "^16.3.1"
}
},
"node_modules/@playwright/test": {
@@ -39,6 +40,19 @@
"undici-types": "~6.20.0"
}
},
+ "node_modules/dotenv": {
+ "version": "16.4.7",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
+ "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
diff --git a/test/e2e/package.json b/test/e2e/package.json
index 98fe62bd8..3df62b045 100644
--- a/test/e2e/package.json
+++ b/test/e2e/package.json
@@ -9,6 +9,7 @@
"description": "",
"devDependencies": {
"@playwright/test": "^1.49.0",
- "@types/node": "^22.10.1"
+ "@types/node": "^22.10.1",
+ "dotenv": "^16.3.1"
}
}
diff --git a/test/e2e/pages/dashboard-page.ts b/test/e2e/pages/dashboard-page.ts
new file mode 100644
index 000000000..2b5299027
--- /dev/null
+++ b/test/e2e/pages/dashboard-page.ts
@@ -0,0 +1,30 @@
+import { expect, type Page, type Locator } from '@playwright/test';
+import exp from 'constants';
+
+export class DashboardPage {
+ readonly page: Page;
+ readonly navBarDashboardBtn: Locator;
+ readonly userMenuAvatar: Locator;
+ readonly logOutMenuItem: Locator;
+
+ constructor(page: Page) {
+ this.page = page;
+ // temporary; update after next deployment when my test-dataid's are deployed
+ //this.navBarDashboardBtn = this.page.getByTestId('nav-bar-dashboard-button');
+ this.navBarDashboardBtn = this.page.getByRole('link', { name: 'Dashboard' });
+ // temporary; update after next deployment when my test-dataid's are deployed
+ //this.userMenuAvatar = this.page.getByTestId('user-menu-avatar');
+ this.userMenuAvatar = this.page.locator('.flex-center');
+ // temporary; update after next deployment when my test-dataid's are deployed
+ //this.logOutMenuItem = this.page.getByTestId('user-nav-logout-menu');
+ this.logOutMenuItem = this.page.getByRole('link', { name: 'Log out' });
+ }
+
+ async logOut() {
+ await expect(this.userMenuAvatar).toBeVisible();
+ await this.userMenuAvatar.click();
+ await expect(this.logOutMenuItem).toBeVisible();
+ await this.logOutMenuItem.click();
+ await expect(this.logOutMenuItem).toBeHidden();
+ }
+}
diff --git a/test/e2e/pages/fxa-page.ts b/test/e2e/pages/fxa-page.ts
new file mode 100644
index 000000000..571f11514
--- /dev/null
+++ b/test/e2e/pages/fxa-page.ts
@@ -0,0 +1,24 @@
+import { expect, type Page, type Locator } from '@playwright/test';
+import { PROD_LOGIN_PWORD } from '../const/constants';
+
+export class FxAPage {
+ readonly page: Page;
+ readonly signInHeaderText: Locator;
+ readonly userAvatar: Locator;
+ readonly passwordInput: Locator;
+ readonly signInButton: Locator;
+
+ constructor(page: Page) {
+ this.page = page;
+ this.signInHeaderText = this.page.getByText('Enter your password');
+ this.userAvatar = this.page.getByTestId('avatar-default');
+ this.passwordInput = this.page.getByRole('textbox', {name: 'password' });
+ this.signInButton = this.page.getByRole('button', { name: 'Sign in' });
+ }
+
+ async signIn() {
+ expect(PROD_LOGIN_PWORD, 'getting APPT_PROD_LOGIN_PWORD env var').toBeTruthy();
+ await this.passwordInput.fill(String(PROD_LOGIN_PWORD));
+ await this.signInButton.click();
+ }
+}
diff --git a/test/e2e/pages/splashscreen-page.ts b/test/e2e/pages/splashscreen-page.ts
index 6ad2cffd3..5de74fcad 100644
--- a/test/e2e/pages/splashscreen-page.ts
+++ b/test/e2e/pages/splashscreen-page.ts
@@ -1,18 +1,51 @@
-import { type Page, type Locator } from '@playwright/test';
-import { APPT_PROD_URL } from '../const/constants';
+import { expect, type Page, type Locator } from '@playwright/test';
+import { APPT_PROD_URL, PROD_LOGIN_EMAIL, FXA_PAGE_TITLE } from '../const/constants';
export class SplashscreenPage {
readonly page: Page;
readonly loginBtn: Locator;
readonly signUpBetaBtn: Locator;
+ readonly loginEmailInput: Locator;
+ readonly loginContinueBtn: Locator;
constructor(page: Page) {
this.page = page;
- this.loginBtn = this.page.getByTestId('home-login-btn');
- this.signUpBetaBtn = this.page.getByTestId('home-sign-up-beta-btn');
+ //update this after data-testid 's are deployed to prod
+ //this.loginBtn = this.page.getByTestId('home-login-btn');
+ this.loginBtn = this.page.getByTitle('Log in');
+ //update this after data-testid 's are deployed to prod
+ //this.signUpBetaBtn = this.page.getByTestId('home-sign-up-beta-btn');
+ this.signUpBetaBtn = this.page.getByTitle('Sign up for the beta');
+ //update this after data-testid 's are deployed to prod
+ //this.loginEmailInput = this.page.getByTestId('login-email-input');
+ this.loginEmailInput = this.page.getByLabel('Email address');
+ //update this after data-testid 's are deployed to prod
+ //this.loginContinueBtn = this.page.getByTestId('login-continue-btn');
+ this.loginContinueBtn = this.page.getByTitle('Continue');
}
async gotoProd() {
await this.page.goto(APPT_PROD_URL);
}
+
+ async clickLoginBtn() {
+ await this.loginBtn.click();
+ }
+
+ async enterLoginEmail(emailAddress: string) {
+ await this.loginEmailInput.fill(emailAddress);
+ }
+
+ async clickLoginContinueBtn() {
+ await this.loginContinueBtn.click();
+ }
+
+ async getToFxA() {
+ await expect(this.loginBtn).toBeVisible();
+ await this.clickLoginBtn();
+ expect(PROD_LOGIN_EMAIL, 'getting APPT_PROD_LOGIN_EMAIL env var').toBeTruthy();
+ await this.enterLoginEmail(String(PROD_LOGIN_EMAIL))
+ await this.clickLoginContinueBtn();
+ await expect(this.page).toHaveTitle(FXA_PAGE_TITLE, { timeout: 30_000 }); // be generous in case FxA is slow to load
+ }
}
diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts
index 2f87197cd..d2704c2fa 100644
--- a/test/e2e/playwright.config.ts
+++ b/test/e2e/playwright.config.ts
@@ -4,9 +4,9 @@ import { defineConfig, devices } from '@playwright/test';
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
-// import dotenv from 'dotenv';
-// import path from 'path';
-// dotenv.config({ path: path.resolve(__dirname, '.env') });
+import dotenv from 'dotenv';
+import path from 'path';
+dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
diff --git a/test/e2e/tests/sign-in.spec.ts b/test/e2e/tests/sign-in.spec.ts
new file mode 100644
index 000000000..45c6da412
--- /dev/null
+++ b/test/e2e/tests/sign-in.spec.ts
@@ -0,0 +1,44 @@
+import { test, expect } from '@playwright/test';
+import { SplashscreenPage } from '../pages/splashscreen-page';
+import { FxAPage } from '../pages/fxa-page';
+import { FXA_PAGE_TITLE, APPT_PAGE_TITLE } from '../const/constants';
+import { DashboardPage } from '../pages/dashboard-page';
+
+let splashscreen: SplashscreenPage;
+let fxa_sign_in: FxAPage;
+let dashboard_page: DashboardPage;
+
+test.beforeEach(async ({ page }) => {
+ // navigate to the main appointment page (splashscreen)
+ splashscreen = new SplashscreenPage(page);
+ fxa_sign_in = new FxAPage(page);
+ dashboard_page = new DashboardPage(page);
+ await splashscreen.gotoProd();
+});
+
+// verify basic sign-in flow works
+test.describe('basic sign-in flow', {
+ tag: '@prod-sanity'
+}, () => {
+ test('clicking login button brings up the sign-in dialog', async ({ page }) => {
+ await expect(splashscreen.loginBtn).toBeVisible();
+ await splashscreen.clickLoginBtn();
+ await expect(splashscreen.loginEmailInput).toBeVisible();
+ await expect(splashscreen.loginContinueBtn).toBeVisible();
+ });
+
+ test('clicking continue button on login dialog brings up the fxa sign-in dialog', async ({ page }) => {
+ await splashscreen.getToFxA();
+ await expect(fxa_sign_in.userAvatar).toBeVisible({ timeout: 30_000}); // generous time for fxa to appear
+ await expect(fxa_sign_in.signInButton).toBeVisible();
+ });
+
+ test('able to sign-in to appointment via fxa', async ({ page }) => {
+ await splashscreen.getToFxA();
+ await expect(fxa_sign_in.signInHeaderText).toBeVisible({ timeout: 30_000 });
+ await fxa_sign_in.signIn();
+ await expect(page).toHaveTitle(APPT_PAGE_TITLE, { timeout: 30_000 }); // give generous time for fxa sign to complete
+ await expect(dashboard_page.userMenuAvatar).toBeVisible({ timeout: 30_000 });
+ await expect(dashboard_page.navBarDashboardBtn).toBeVisible({ timeout: 30_000 });
+ });
+});
diff --git a/test/e2e/tests/sign-out.spec.ts b/test/e2e/tests/sign-out.spec.ts
new file mode 100644
index 000000000..1ba133d14
--- /dev/null
+++ b/test/e2e/tests/sign-out.spec.ts
@@ -0,0 +1,32 @@
+import { test, expect } from '@playwright/test';
+import { SplashscreenPage } from '../pages/splashscreen-page';
+import { FxAPage } from '../pages/fxa-page';
+import { DashboardPage } from '../pages/dashboard-page';
+import { APPT_PAGE_TITLE } from '../const/constants';
+
+let splashscreen: SplashscreenPage;
+let fxa_sign_in: FxAPage;
+let dashboard_page: DashboardPage;
+
+test.beforeEach(async ({ page }) => {
+ // navigate to the main appointment page (splashscreen) and sign-in via fxa
+ splashscreen = new SplashscreenPage(page);
+ fxa_sign_in = new FxAPage(page);
+ dashboard_page = new DashboardPage(page);
+ await splashscreen.gotoProd();
+ await splashscreen.getToFxA();
+ await expect(fxa_sign_in.signInHeaderText).toBeVisible({ timeout: 30_000 });
+ await fxa_sign_in.signIn();
+ await expect(dashboard_page.userMenuAvatar).toBeVisible({ timeout: 30_000 });
+});
+
+// verify basic sign-out flow works
+test.describe('basic sign-out flow', {
+ tag: '@prod-sanity'
+}, () => {
+ test('able to log out of appointment', async ({ page }) => {
+ await dashboard_page.logOut();
+ await expect(page).toHaveTitle(APPT_PAGE_TITLE, { timeout: 30_000 }); // generous time for fxa sign-out
+ await expect(splashscreen.loginBtn).toBeVisible({ timeout: 30_000 });
+ });
+});