From d908ca359dba4d0f8eff6e43f6bd7af2114149d6 Mon Sep 17 00:00:00 2001
From: Bryce Barbara
Date: Wed, 5 Oct 2022 13:37:01 -0700
Subject: [PATCH] Improve e2e tests
---
docs/authentication.md | 1 -
eng/scripts/populate-firebase.js | 44 ++++++------
frontend/.gitignore | 3 +
frontend/cypress.config.ts | 6 +-
frontend/cypress.env.json.example | 3 +
frontend/cypress/e2e/api-test-page.spec.ts | 69 +++++++++++++++++++
frontend/cypress/e2e/basic.spec.ts | 36 ----------
frontend/cypress/e2e/login.spec.ts | 26 +++++++
frontend/cypress/support/e2e.ts | 12 ++++
frontend/package.json | 2 +
frontend/serviceAccount.json.example | 12 ++++
frontend/src/components/layout/Header.vue | 8 ++-
frontend/src/components/layout/Sidebar.vue | 1 +
frontend/src/components/pages/ApiTestPage.vue | 22 ++++--
frontend/src/modules/auth-guard.ts | 5 --
frontend/src/pages/login.vue | 2 +-
frontend/vite.config.ts | 2 +-
pnpm-lock.yaml | 68 ++++++++++++++++++
18 files changed, 249 insertions(+), 73 deletions(-)
create mode 100644 frontend/.gitignore
create mode 100644 frontend/cypress.env.json.example
create mode 100644 frontend/cypress/e2e/api-test-page.spec.ts
delete mode 100644 frontend/cypress/e2e/basic.spec.ts
create mode 100644 frontend/cypress/e2e/login.spec.ts
create mode 100644 frontend/cypress/support/e2e.ts
create mode 100644 frontend/serviceAccount.json.example
diff --git a/docs/authentication.md b/docs/authentication.md
index b10788e..cb9bbec 100644
--- a/docs/authentication.md
+++ b/docs/authentication.md
@@ -23,6 +23,5 @@ TODO: Add a description for this document and what readers can expect to learn f
meta:
requiresAuth: false # indicates whether the route requires authentication
- requiresGuest: true # indicates whether the route is only available to guests
```
diff --git a/eng/scripts/populate-firebase.js b/eng/scripts/populate-firebase.js
index 45c886a..10dcd9a 100644
--- a/eng/scripts/populate-firebase.js
+++ b/eng/scripts/populate-firebase.js
@@ -31,28 +31,7 @@ const firebaseAppDetailDefaults = {
class Script {
/**
- *
- * @param {string[]} args
- * @returns {ScriptOptions}
- */
- static getScriptOptionsFromArgs(args) {
- /** @type {ScriptOptions} */
- const options = {
- detailsFileName: defaultDetailsFileName,
- forceEnabled: false,
- }
-
- options.forceEnabled = args.includes('--force')
- if (options.forceEnabled)
- args = args.filter(arg => arg !== '--force')
- if (args.length > 0 && args[0])
- options.detailsFileName = args[0]
-
- return options
- }
-
- /**
- *
+ * The main entry point for the script.
* @param {string[]} args
*/
static async main(args) {
@@ -77,6 +56,27 @@ class Script {
const authProviderFilePath = path.join(process.cwd(), 'frontend/src/firebase/auth-provider.g.ts')
FirebaseTools.updateAuthProviderTs(authProviderFilePath, fbDetails, options)
}
+
+ /**
+ * Attempts to parse the script options from the given command line arguments.
+ * @param {string[]} args The command line arguments.
+ * @returns {ScriptOptions}
+ */
+ static getScriptOptionsFromArgs(args) {
+ /** @type {ScriptOptions} */
+ const options = {
+ detailsFileName: defaultDetailsFileName,
+ forceEnabled: false,
+ }
+
+ options.forceEnabled = args.includes('--force')
+ if (options.forceEnabled)
+ args = args.filter(arg => arg !== '--force')
+ if (args.length > 0 && args[0])
+ options.detailsFileName = args[0]
+
+ return options
+ }
}
class FirebaseTools {
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..34a918f
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,3 @@
+# Avoid commiting sensitive data
+cypress.env.json
+serviceAccount.json
diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts
index de709e5..642e103 100644
--- a/frontend/cypress.config.ts
+++ b/frontend/cypress.config.ts
@@ -1,10 +1,14 @@
import { defineConfig } from 'cypress'
+import admin from 'firebase-admin'
+import { plugin as cypressFirebasePlugin } from 'cypress-firebase'
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3333',
chromeWebSecurity: false,
specPattern: 'cypress/e2e/**/*.spec.*',
- supportFile: false,
+ setupNodeEvents(on, config) {
+ cypressFirebasePlugin(on, config, admin)
+ },
},
})
diff --git a/frontend/cypress.env.json.example b/frontend/cypress.env.json.example
new file mode 100644
index 0000000..4f0218c
--- /dev/null
+++ b/frontend/cypress.env.json.example
@@ -0,0 +1,3 @@
+{
+ "TEST_UID": "0000000000000000000000000000"
+}
diff --git a/frontend/cypress/e2e/api-test-page.spec.ts b/frontend/cypress/e2e/api-test-page.spec.ts
new file mode 100644
index 0000000..c4394a9
--- /dev/null
+++ b/frontend/cypress/e2e/api-test-page.spec.ts
@@ -0,0 +1,69 @@
+const Locators = {
+ welcomeMessageRefreshButton: '[data-test=welcome-message-refresh-button]',
+ welcomeMessageResult: '[data-test=welcome-message-result]',
+ remoteMathInput: '[data-test=remote-math-input]',
+ remoteMathSubmitButton: '[data-test=remote-math-submit-button]',
+ remoteMathResultOutput: '[data-test=remote-math-result-output]',
+ remoteMathResultWhen: '[data-test=remote-math-result-when]',
+}
+
+function normalizeNumberString(str: string) {
+ return str.replace(/[^0-9.]/g, '')
+}
+
+context('API Test Page', () => {
+ before(() => {
+ cy.login()
+ .visit('/api-test')
+ })
+
+ context('Welcome Message', () => {
+ it('Can refresh welcome message', () => {
+ cy.intercept('POST', '/*/*/getWelcomeMessage')
+ .as('getWelcomeMessage')
+
+ // The refresh button for the welcome message should be visible
+ cy.get(Locators.welcomeMessageRefreshButton)
+ .should('be.visible')
+
+ // Clicking the refresh button should trigger a request to the API
+ cy.get(Locators.welcomeMessageRefreshButton)
+ .click()
+ .wait('@getWelcomeMessage')
+
+ // The result should be visible
+ cy.get(Locators.welcomeMessageResult)
+ .should('be.visible')
+ })
+ })
+
+ context('Remote Math', () => {
+ it('Can get accurate results', () => {
+ const inputs = [123, 456]
+
+ cy.intercept('POST', '/*/*/calculateSquare')
+ .as('calculateSquare')
+
+ for (const input of inputs) {
+ cy.get(Locators.remoteMathInput)
+ .clear()
+ .type(input.toString())
+
+ // Clicking the submit button should trigger a request to the API
+ cy.get(Locators.remoteMathSubmitButton)
+ .click()
+ .wait('@calculateSquare')
+
+ // The result should be visible
+ cy.get(Locators.remoteMathResultWhen)
+ .should('be.visible')
+ cy.get(Locators.remoteMathResultOutput)
+ .should('be.visible')
+ .should((output) => {
+ // The result should be accurate
+ expect(normalizeNumberString(output.text())).to.eq((input * input).toString())
+ })
+ }
+ })
+ })
+})
diff --git a/frontend/cypress/e2e/basic.spec.ts b/frontend/cypress/e2e/basic.spec.ts
deleted file mode 100644
index 55a838f..0000000
--- a/frontend/cypress/e2e/basic.spec.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-context('Basic', () => {
- beforeEach(() => {
- cy.visit('/')
- })
-
- it('basic nav', () => {
- cy.url()
- .should('eq', 'http://localhost:3333/')
-
- cy.contains('[Home Layout]')
- .should('exist')
-
- cy.get('#input')
- .type('Vitesse{Enter}')
- .url()
- .should('eq', 'http://localhost:3333/hi/Vitesse')
-
- cy.contains('[Default Layout]')
- .should('exist')
-
- cy.get('[btn]')
- .click()
- .url()
- .should('eq', 'http://localhost:3333/')
- })
-
- it('markdown', () => {
- cy.get('[title="About"]')
- .click()
- .url()
- .should('eq', 'http://localhost:3333/about')
-
- cy.get('.shiki')
- .should('exist')
- })
-})
diff --git a/frontend/cypress/e2e/login.spec.ts b/frontend/cypress/e2e/login.spec.ts
new file mode 100644
index 0000000..9a12d69
--- /dev/null
+++ b/frontend/cypress/e2e/login.spec.ts
@@ -0,0 +1,26 @@
+context('Login', () => {
+ it('Detects logging in', () => {
+ cy.logout()
+ .visit('/')
+ .url()
+ .should('eq', 'http://localhost:3333/')
+
+ cy.get('[data-test=login-button]')
+ .should('exist')
+
+ // Login which app should detect
+ cy.login()
+
+ cy.get('[data-test=logout-button]')
+ .should('exist')
+ })
+
+ it('Can log out', () => {
+ cy.get('[data-test=logout-button]')
+ .should('exist')
+ .click()
+
+ cy.get('[data-test=login-button]')
+ .should('exist')
+ })
+})
diff --git a/frontend/cypress/support/e2e.ts b/frontend/cypress/support/e2e.ts
new file mode 100644
index 0000000..9956610
--- /dev/null
+++ b/frontend/cypress/support/e2e.ts
@@ -0,0 +1,12 @@
+import firebase from 'firebase/compat/app'
+import 'firebase/compat/auth'
+import 'firebase/compat/database'
+import 'firebase/compat/firestore'
+import { attachCustomCommands } from 'cypress-firebase'
+import { app } from '../../src/firebase/app.g'
+
+firebase.initializeApp({
+ ...app.options,
+})
+
+attachCustomCommands({ Cypress, cy, firebase })
diff --git a/frontend/package.json b/frontend/package.json
index e582cb3..22398f0 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -44,8 +44,10 @@
"critters": "^0.0.16",
"cross-env": "^7.0.3",
"cypress": "^10.9.0",
+ "cypress-firebase": "^2.2.2",
"eslint": "^8.24.0",
"eslint-plugin-cypress": "^2.12.1",
+ "firebase-admin": "^10.3.0",
"https-localhost": "^4.7.1",
"jsdom": "^20.0.1",
"markdown-it-link-attributes": "^4.0.1",
diff --git a/frontend/serviceAccount.json.example b/frontend/serviceAccount.json.example
new file mode 100644
index 0000000..cb683b9
--- /dev/null
+++ b/frontend/serviceAccount.json.example
@@ -0,0 +1,12 @@
+{
+ "type": "service_account",
+ "project_id": "",
+ "private_key_id": "",
+ "private_key": "",
+ "client_email": "",
+ "client_id": "",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://oauth2.googleapis.com/token",
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+ "client_x509_cert_url": ""
+}
diff --git a/frontend/src/components/layout/Header.vue b/frontend/src/components/layout/Header.vue
index 391d3d4..6af5463 100644
--- a/frontend/src/components/layout/Header.vue
+++ b/frontend/src/components/layout/Header.vue
@@ -21,6 +21,7 @@ const showLanguageSelector = computed(() => availableLocales.length > 1)
:title="t('button.toggle-sidebar')"
class="sidebar-toggle"
:class="{ flipped: sidebarCollapsed }"
+ data-test="sidebar-toggle-button"
@click="() => toggleSidebar()"
>
@@ -32,7 +33,12 @@ const showLanguageSelector = computed(() => availableLocales.length > 1)
-
+
{{ t('button.sign-out') }}
diff --git a/frontend/src/components/layout/Sidebar.vue b/frontend/src/components/layout/Sidebar.vue
index 77a6362..d5dbfc0 100644
--- a/frontend/src/components/layout/Sidebar.vue
+++ b/frontend/src/components/layout/Sidebar.vue
@@ -100,5 +100,6 @@ function findMatchingMenuToRoute(routePath: string): MenuOption | null {
:collapsed-width="sidebarCollapsedWidth"
:options="menuOptions"
:render-label="renderMenuLabel"
+ data-test="sidebar"
/>
diff --git a/frontend/src/components/pages/ApiTestPage.vue b/frontend/src/components/pages/ApiTestPage.vue
index 46a4f87..acedd2a 100644
--- a/frontend/src/components/pages/ApiTestPage.vue
+++ b/frontend/src/components/pages/ApiTestPage.vue
@@ -47,14 +47,14 @@ const remoteMathErrorMessage = computed(() => {
{{ welcomeErrorMessage }}
-
+
{{ t('pages.api-test.welcome-test.label.server-result') }}
{{ welcomeMessage.state.value.message }}
- welcomeMessage.execute()">
+ welcomeMessage.execute()">
{{ t('pages.api-test.welcome-test.button.refresh') }}
@@ -72,10 +72,11 @@ const remoteMathErrorMessage = computed(() => {
:placeholder="t('pages.api-test.remote-math-test.placeholder.number-input')"
clearable
min-w-280px
+ data-test="remote-math-input"
/>
-
+
{{ t('pages.api-test.remote-math-test.button.submit') }}
@@ -84,8 +85,19 @@ const remoteMathErrorMessage = computed(() => {
{{ t('pages.api-test.remote-math-test.label.server-result') }}
-
- {{ t('pages.api-test.remote-math-test.result', { output: n(mathTest.state.value.output), when: d(mathTest.state.value.date, 'long') }) }}
+
+
+
+ {{ n(mathTest.state.value.output) }}
+
+
+ {{ d(mathTest.state.value.date, 'long') }}
+
+
{{ t('pages.api-test.remote-math-test.error.missing-result') }}
diff --git a/frontend/src/modules/auth-guard.ts b/frontend/src/modules/auth-guard.ts
index eaeb997..036de76 100644
--- a/frontend/src/modules/auth-guard.ts
+++ b/frontend/src/modules/auth-guard.ts
@@ -3,16 +3,11 @@ import { type UserModule } from '~/types'
export const install: UserModule = ({ router }) => {
router.beforeEach(async (to, from, next) => {
const requiresAuth = to.matched.some(x => x.meta.requiresAuth)
- const requiresGuest = to.matched.some(x => x.meta.requiresGuest)
if (requiresAuth && !auth.currentUser) {
// console.log('Redirecting to login...')
next({ name: 'login' })
}
- else if (requiresGuest && auth.currentUser) {
- // console.log('Redirecting to home...')
- next('/')
- }
else {
next()
}
diff --git a/frontend/src/pages/login.vue b/frontend/src/pages/login.vue
index 00111fc..63cc5fd 100644
--- a/frontend/src/pages/login.vue
+++ b/frontend/src/pages/login.vue
@@ -80,6 +80,7 @@ onMounted(() => {
:disabled="isSigningIn"
type="primary"
important-w-full
+ data-test="login-button"
@click="signIn"
>
{{ t('button.sign-in') }}
@@ -98,6 +99,5 @@ onMounted(() => {
meta:
requiresAuth: false
- requiresGuest: true
layout: bare
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 44d8fd1..75660bb 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -36,7 +36,7 @@ export default defineConfig({
extensions: ['vue', 'md'],
extendRoute(route) {
// Any route that isn't explicitly set not to public will be made to require auth
- const isAnonymous = route.meta && (route.meta.requiresAuth === false || route.meta.requiresGuest === true)
+ const isAnonymous = route.meta && (route.meta.requiresAuth === false)
if (isAnonymous)
return route
// Augment the route with meta that indicates that the route requires authentication.
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b7c94a4..504a754 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -53,9 +53,11 @@ importers:
critters: ^0.0.16
cross-env: ^7.0.3
cypress: ^10.9.0
+ cypress-firebase: ^2.2.2
eslint: ^8.24.0
eslint-plugin-cypress: ^2.12.1
firebase: ^9.10.0
+ firebase-admin: ^10.3.0
https-localhost: ^4.7.1
jsdom: ^20.0.1
ky: ^0.31.3
@@ -112,8 +114,10 @@ importers:
critters: 0.0.16
cross-env: 7.0.3
cypress: 10.9.0
+ cypress-firebase: 2.2.2_firebase-admin@10.3.0
eslint: 8.24.0
eslint-plugin-cypress: 2.12.1_eslint@8.24.0
+ firebase-admin: 10.3.0
https-localhost: 4.7.1
jsdom: 20.0.1
markdown-it-link-attributes: 4.0.1
@@ -1622,6 +1626,15 @@ packages:
- utf-8-validate
dev: false
+ /@firebase/auth-interop-types/0.1.6_@firebase+util@1.6.3:
+ resolution: {integrity: sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==}
+ peerDependencies:
+ '@firebase/app-types': 0.x
+ '@firebase/util': 1.x
+ dependencies:
+ '@firebase/util': 1.6.3
+ dev: true
+
/@firebase/auth-interop-types/0.1.6_pbfwexsq7uf6mrzcwnikj3g37m:
resolution: {integrity: sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==}
peerDependencies:
@@ -1665,6 +1678,19 @@ packages:
'@firebase/util': 1.6.3
tslib: 2.4.0
+ /@firebase/database-compat/0.2.6:
+ resolution: {integrity: sha512-Ls1BAODaiDYgeJljrIgSuC7JkFIY/HNhhNYebzZSoGQU62RuvnaO3Qgp2EH6h2LzHyRnycNadfh1suROtPaUIA==}
+ dependencies:
+ '@firebase/component': 0.5.17
+ '@firebase/database': 0.13.6
+ '@firebase/database-types': 0.9.13
+ '@firebase/logger': 0.3.3
+ '@firebase/util': 1.6.3
+ tslib: 2.4.0
+ transitivePeerDependencies:
+ - '@firebase/app-types'
+ dev: true
+
/@firebase/database-compat/0.2.6_@firebase+app-types@0.7.0:
resolution: {integrity: sha512-Ls1BAODaiDYgeJljrIgSuC7JkFIY/HNhhNYebzZSoGQU62RuvnaO3Qgp2EH6h2LzHyRnycNadfh1suROtPaUIA==}
dependencies:
@@ -1683,6 +1709,19 @@ packages:
'@firebase/app-types': 0.7.0
'@firebase/util': 1.6.3
+ /@firebase/database/0.13.6:
+ resolution: {integrity: sha512-5IZIBw2LT50Z8mwmKYmdX37p+Gg2HgeJsrruZmRyOSVgbfoY4Pg87n1uFx6qWqDmfL6HwQgwcrrQfVIXE3C5SA==}
+ dependencies:
+ '@firebase/auth-interop-types': 0.1.6_@firebase+util@1.6.3
+ '@firebase/component': 0.5.17
+ '@firebase/logger': 0.3.3
+ '@firebase/util': 1.6.3
+ faye-websocket: 0.11.4
+ tslib: 2.4.0
+ transitivePeerDependencies:
+ - '@firebase/app-types'
+ dev: true
+
/@firebase/database/0.13.6_@firebase+app-types@0.7.0:
resolution: {integrity: sha512-5IZIBw2LT50Z8mwmKYmdX37p+Gg2HgeJsrruZmRyOSVgbfoY4Pg87n1uFx6qWqDmfL6HwQgwcrrQfVIXE3C5SA==}
dependencies:
@@ -4031,6 +4070,14 @@ packages:
resolution: {integrity: sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==}
dev: false
+ /cypress-firebase/2.2.2_firebase-admin@10.3.0:
+ resolution: {integrity: sha512-EfpSJ8z8WXnIM1ezbxFUk51Kt6jaFFnEZp6uOoSgb9DF58hwxJ+MoSw1rpyX8Kwuiv/14pFpDiI1U+ZvSTK5fg==}
+ peerDependencies:
+ firebase-admin: '>=8'
+ dependencies:
+ firebase-admin: 10.3.0
+ dev: true
+
/cypress/10.9.0:
resolution: {integrity: sha512-MjIWrRpc+bQM9U4kSSdATZWZ2hUqHGFEQTF7dfeZRa4MnalMtc88FIE49USWP2ZVtfy5WPBcgfBX+YorFqGElA==}
engines: {node: '>=12.0.0'}
@@ -5398,6 +5445,27 @@ packages:
path-exists: 4.0.0
dev: true
+ /firebase-admin/10.3.0:
+ resolution: {integrity: sha512-A0wgMLEjyVyUE+heyMJYqHRkPVjpebhOYsa47RHdrTM4ltApcx8Tn86sUmjqxlfh09gNnILAm7a8q5+FmgBYpg==}
+ engines: {node: '>=12.7.0'}
+ dependencies:
+ '@fastify/busboy': 1.1.0
+ '@firebase/database-compat': 0.2.6
+ '@firebase/database-types': 0.9.13
+ '@types/node': 18.8.2
+ jsonwebtoken: 8.5.1
+ jwks-rsa: 2.1.4
+ node-forge: 1.3.1
+ uuid: 8.3.2
+ optionalDependencies:
+ '@google-cloud/firestore': 4.15.1
+ '@google-cloud/storage': 5.20.5
+ transitivePeerDependencies:
+ - '@firebase/app-types'
+ - encoding
+ - supports-color
+ dev: true
+
/firebase-admin/10.3.0_@firebase+app-types@0.7.0:
resolution: {integrity: sha512-A0wgMLEjyVyUE+heyMJYqHRkPVjpebhOYsa47RHdrTM4ltApcx8Tn86sUmjqxlfh09gNnILAm7a8q5+FmgBYpg==}
engines: {node: '>=12.7.0'}