Skip to content

Commit 1cf5e2a

Browse files
authored
Run E2E tests on PRs and wait till deploys reach steady state. (Fixes #172) (#814)
1 parent 2a6c310 commit 1cf5e2a

7 files changed

Lines changed: 91 additions & 37 deletions

File tree

.github/workflows/merge.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,10 @@ jobs:
348348

349349
e2e-tests-browserstack-stage:
350350
name: e2e-tests-browserstack-stage
351-
needs: accounts-deploy
352-
if: '!cancelled()'
351+
needs:
352+
- accounts-deploy
353+
- keycloak-deploy
354+
if: always() && needs.accounts-deploy.result == 'success' && needs.keycloak-deploy.result != 'failure' && needs.keycloak-deploy.result != 'cancelled'
353355
runs-on: ubuntu-latest
354356
environment:
355357
name: staging

.github/workflows/validate.yml

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
name: validate
33

44
concurrency:
5-
group: validate
5+
group: validate-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
66
cancel-in-progress: true
77

88
on:
9+
pull_request:
10+
branches:
11+
- main
912
push:
1013
branches:
11-
- '**'
12-
- '!main'
14+
- main
1315
# Allows you to run this workflow manually from the Actions tab
1416
workflow_dispatch:
1517

@@ -25,6 +27,7 @@ jobs:
2527
IS_CI_AUTOMATION: "yes"
2628
outputs:
2729
src-changed: ${{ steps.check.outputs.src-changed }}
30+
e2e-changed: ${{ steps.check.outputs.e2e-changed }}
2831
iac-changed: ${{ steps.check.outputs.iac-changed }}
2932
steps:
3033
- uses: actions/checkout@v4
@@ -51,6 +54,14 @@ jobs:
5154
- 'src/**'
5255
- 'uv.lock'
5356
- 'vite.config.mjs'
57+
e2e-changed:
58+
- '.github/workflows/validate.yml'
59+
- '.env.example'
60+
- 'config.toml.example'
61+
- 'docker-compose.yml'
62+
- 'Dockerfile.keycloak'
63+
- 'keycloak/**'
64+
- 'test/e2e/**'
5465
iac-changed:
5566
- 'pulumi/**'
5667
@@ -107,8 +118,15 @@ jobs:
107118
src: "pulumi"
108119

109120
run-e2e-tests-local:
110-
needs: detect-changes
111-
if: false
121+
needs:
122+
- detect-changes
123+
- validate-accounts
124+
if: >-
125+
always() &&
126+
(needs.detect-changes.outputs.src-changed == 'true' || needs.detect-changes.outputs.e2e-changed == 'true') &&
127+
needs.detect-changes.result == 'success' &&
128+
needs.validate-accounts.result != 'failure' &&
129+
needs.validate-accounts.result != 'cancelled'
112130
runs-on: ubuntu-latest
113131
environment:
114132
name: staging
@@ -129,31 +147,47 @@ jobs:
129147
- uses: actions/checkout@v4
130148

131149
- name: Set up local environment
150+
shell: bash
132151
run: |
133152
cp .env.example .env
134153
mkdir -p mail/etc && cp config.toml.example mail/etc/config.toml
135-
echo -e "\nFXA_CLIENT_ID=$FXA_CLIENT_ID" >> .env
136-
echo "FXA_SECRET=$FXA_SECRET" >> .env
137-
echo "SECRET_KEY=$SECRET_KEY" >> .env
138-
echo "LOGIN_CODE_SECRET=$LOGIN_CODE_SECRET" >> .env
139-
echo "FXA_ENCRYPT_SECRET=$FXA_ENCRYPT_SECRET" >> .env
140-
echo "PADDLE_TOKEN=$PADDLE_TOKEN" >> .env
141-
echo "PADDLE_API_KEY=$PADDLE_API_KEY" >> .env
142-
143-
- name: Start appointment stack via docker
154+
append_env_if_set() {
155+
local key="$1"
156+
local value="${!key}"
157+
158+
if [ -n "$value" ]; then
159+
printf '%s=%s\n' "$key" "$value" >> .env
160+
fi
161+
}
162+
163+
echo >> .env
164+
append_env_if_set FXA_CLIENT_ID
165+
append_env_if_set FXA_SECRET
166+
append_env_if_set SECRET_KEY
167+
append_env_if_set LOGIN_CODE_SECRET
168+
append_env_if_set FXA_ENCRYPT_SECRET
169+
append_env_if_set PADDLE_TOKEN
170+
append_env_if_set PADDLE_API_KEY
171+
172+
- name: Start accounts stack via docker
144173
run: docker compose up -d --build -V
145174

175+
- name: Wait for local services
176+
run: |
177+
curl --retry 30 --retry-all-errors --retry-delay 2 --fail http://localhost:8087/health
178+
curl --retry 30 --retry-all-errors --retry-delay 2 --fail http://localhost:9000/health/ready
179+
146180
- uses: actions/setup-node@v4
147181
with:
148182
node-version: 20
149183
cache: 'npm'
150184
cache-dependency-path: 'test/e2e/package-lock.json'
151185

152186
- name: Install E2E test dependencies
187+
working-directory: test/e2e
153188
run: |
154-
cd ./test/e2e
155-
npm install
156-
npx playwright install
189+
npm ci
190+
npx playwright install --with-deps firefox
157191
158192
- name: Create test plans
159193
run: |
@@ -163,8 +197,8 @@ jobs:
163197
164198
- name: Run E2E tests against local stack
165199
id: e2e
200+
working-directory: test/e2e
166201
run: |
167-
cd ./test/e2e
168202
cp .env.dev.example .env
169203
npm run e2e-test
170204

pulumi/__main__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@
1717
MSG_LB_MATCHING_CLUSTER = 'In this stack, Fargate clusters must have matching load balancer security groups.'
1818
MSG_CONTAINER_MATCHING_CLUSTER = 'In this stack, Fargate clusters must have matching container security groups.'
1919

20+
21+
def wait_for_ecs_service_steady_state(args):
22+
if args.type_ != 'aws:ecs/service:Service':
23+
return None
24+
25+
props = dict(args.props)
26+
props['wait_for_steady_state'] = True
27+
return pulumi.ResourceTransformationResult(props, args.opts)
28+
29+
2030
# Set up the project and convenient config access
2131
project = tb_pulumi.ThunderbirdPulumiProject()
2232
resources = project.config.get('resources')
@@ -113,7 +123,10 @@
113123
container_security_groups=[container_sgs[service].resources['sg'].id],
114124
load_balancer_security_groups=lb_sg_ids,
115125
desired_count=None if autoscaler_opts is not None else desired_count,
116-
opts=pulumi.ResourceOptions(depends_on=depends_on),
126+
opts=pulumi.ResourceOptions(
127+
depends_on=depends_on,
128+
transformations=[wait_for_ecs_service_steady_state],
129+
),
117130
**opts,
118131
)
119132
if autoscaler_opts is not None:

test/e2e/playwright.config.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ import dotenv from 'dotenv';
88
import path from 'path';
99
dotenv.config({ path: path.resolve(__dirname, '.env') });
1010

11+
const ciFirefoxLaunchOptions = process.env.CI ? {
12+
firefoxUserPrefs: {
13+
'network.dns.disableIPv6': true,
14+
'network.dns.native-is-localhost': true,
15+
},
16+
} : undefined;
17+
1118
/**
1219
* See https://playwright.dev/docs/test-configuration.
1320
*/
@@ -52,7 +59,8 @@ export default defineConfig({
5259
{ name: 'desktop-setup',
5360
testMatch: /.*\.desktop.setup\.ts/,
5461
use: {
55-
...devices['Desktop Chrome'],
62+
...devices['Desktop Firefox'],
63+
launchOptions: ciFirefoxLaunchOptions,
5664
screenshot: 'only-on-failure',
5765
},
5866
},
@@ -73,6 +81,7 @@ export default defineConfig({
7381
name: 'firefox',
7482
use: {
7583
...devices['Desktop Firefox'],
84+
launchOptions: ciFirefoxLaunchOptions,
7685
screenshot: 'only-on-failure',
7786
// Use prepared auth state
7887
storageState: 'test-results/.auth/user.json',

test/e2e/tests/auth.desktop.setup.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,17 @@ import { navigateToAccountsHubAndSignIn } from '../utils/utils';
88
const fs = require('fs');
99
const directoryPath = path.join(__dirname, '../test-results/.auth');
1010

11-
fs.mkdir(directoryPath, (err: any) => {
12-
if (err) {
13-
console.error('error creating auth storage directory:', err);
14-
return
15-
}
16-
17-
console.log('created auth storage directory');
18-
// when use storageState in browserstack yml, browserstack requires the file to exist already even on the
19-
// first time the auth-setup step is run; so must create an emtpy user.json file here
20-
const filepath = path.join(directoryPath, 'user.json');
21-
const emptyJsonObject = {};
22-
const jsonString = JSON.stringify(emptyJsonObject, null, 2); // The '2' adds indentation for readability
11+
// when use storageState in browserstack yml, browserstack requires the file to exist already even on the
12+
// first time the auth-setup step is run; so must create an empty user.json file here
13+
const filepath = path.join(directoryPath, 'user.json');
14+
const emptyJsonObject = {};
15+
const jsonString = JSON.stringify(emptyJsonObject, null, 2); // The '2' adds indentation for readability
16+
try {
17+
fs.mkdirSync(directoryPath, { recursive: true });
2318
fs.writeFileSync(filepath, jsonString);
24-
});
19+
} catch (error) {
20+
throw new Error(`Failed to initialize auth storage file at ${filepath}`, { cause: error });
21+
}
2522

2623
// We write it here so it is blown away and re-created at the start of every test run; and is in .gitignore
2724
const authFile = path.join(__dirname, '../test-results/.auth/user.json');

test/e2e/tests/contact.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,6 @@ test.describe('contact support form', {
152152
test('contact form displayed correctly when not signed in', async ({ page }) => {
153153
// clear authentication state for this test
154154
await page.context().clearCookies();
155-
await page.reload();
156155
await page.waitForTimeout(TIMEOUT_5_SECONDS);
157156
await contactPage.navigateToContactPage();
158157

test/e2e/utils/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const navigateToAccountsHubAndSignIn = async (page: Page) => {
4747
const tbAcctsSignInPage = new TBAcctsOIDCPage(page);
4848
const tbAcctsHubPage = new TBAcctsHubPage(page);
4949

50-
await page.goto(`${ACCTS_HUB_URL}`);
50+
await page.goto(`${ACCTS_HUB_URL}`, { waitUntil: 'domcontentloaded' });
5151
//await page.waitForTimeout(TIMEOUT_5_SECONDS);
5252
await waitForVueApp(page);
5353

0 commit comments

Comments
 (0)