Skip to content

Commit a1caa66

Browse files
Part 6 pw (#50)
Local supabase integration created Playwright installed end to end tests added
1 parent b091375 commit a1caa66

25 files changed

+671
-33
lines changed

.github/workflows/e2e-tests.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: E2E Tests with Local Supabase
2+
3+
on:
4+
push:
5+
branches: [ main, develop ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
e2e:
11+
if: "!contains(github.event.head_commit.message, 'E2E')"
12+
runs-on: ubuntu-latest
13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: actions/setup-node@v4
20+
with: { node-version: 20, cache: npm }
21+
- run: npm ci
22+
- run: |
23+
node - <<'NODE'
24+
const v = require('./package.json').devDependencies.rollup ?? 'latest';
25+
console.log('ℹ Installing native Rollup helper for', v);
26+
NODE
27+
- run: npm install --no-save @rollup/rollup-linux-x64-gnu@$(node -p "require('./package.json').devDependencies.rollup || '4'")
28+
29+
- uses: supabase/setup-cli@v1
30+
with: { version: 2.24.3 }
31+
32+
- name: Start Supabase
33+
env: { SUPABASE_TELEMETRY_DISABLED: "1" }
34+
run: supabase start &
35+
36+
- name: Wait for Supabase (≤180 s)
37+
run: npx --yes wait-on tcp:127.0.0.1:54321 tcp:127.0.0.1:54322 --timeout 180000
38+
39+
- run: echo "SUPABASE_URL=http://127.0.0.1:54321" >> $GITHUB_ENV
40+
- run: |
41+
echo "SUPABASE_ANON_KEY=$(supabase status -o env | grep SUPABASE_ANON_KEY | cut -d= -f2)" >> $GITHUB_ENV
42+
43+
- run: npx playwright install --with-deps
44+
- run: npm run e2e:local
45+
env: { CI: "true" }
46+
47+
- uses: actions/upload-artifact@v4
48+
if: always()
49+
with: { name: playwright-report, path: playwright-report }

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,8 @@ Thumbs.db
4747
.runtimeconfig.json
4848
adminSdkConf.json
4949

50+
51+
# Playwright
52+
/test-results/
53+
/playwright-report/
54+
/playwright/.cache/

angular.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,20 @@
127127
],
128128
"scripts": []
129129
}
130+
},
131+
"e2e": {
132+
"builder": "playwright-ng-schematics:playwright",
133+
"options": {
134+
"devServerTarget": "angularblogapp:serve"
135+
},
136+
"configurations": {
137+
"production": {
138+
"devServerTarget": "angularblogapp:serve:production"
139+
},
140+
"local": {
141+
"devServerTarget": "angularblogapp:serve:local"
142+
}
143+
}
130144
}
131145
}
132146
}

e2e/example.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { test, expect } from '@playwright/test';
2+
import { acceptCookies } from './helpers/cookie-consent.helper';
3+
4+
test('has title', async ({ page }) => {
5+
await page.goto('/');
6+
7+
// Handle cookie consent using helper
8+
await acceptCookies(page);
9+
10+
// Expect a title "to contain" a substring.
11+
await expect(page).toHaveTitle(/AngularBlogApp/);
12+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Page, expect } from '@playwright/test';
2+
3+
export async function acceptCookies(page: Page): Promise<void> {
4+
const cookieConsentDialog = page
5+
.getByLabel('Cookie Consent')
6+
.locator('div')
7+
.filter({ hasText: 'Cookie Consent Consent' })
8+
.nth(1);
9+
10+
await expect(cookieConsentDialog).toBeVisible();
11+
12+
const allowAllButton = page.getByRole('button', { name: 'Allow All' });
13+
await allowAllButton.click();
14+
15+
await expect(cookieConsentDialog).not.toBeVisible();
16+
}

e2e/helpers/debug.helper.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { Page } from '@playwright/test';
2+
3+
export async function debugPageState(page: Page, testName: string) {
4+
console.log(`=== DEBUG INFO FOR: ${testName} ===`);
5+
6+
// 1. Check current URL
7+
console.log('Current URL:', page.url());
8+
9+
// 2. Check if Angular app is loaded
10+
const angularLoaded = await page.evaluate(() => {
11+
return !!(window as any).ng;
12+
});
13+
console.log('Angular loaded:', angularLoaded);
14+
15+
// 3. Check network requests
16+
const allRequests: string[] = [];
17+
page.on('request', (request) => {
18+
allRequests.push(`${request.method()} ${request.url()}`);
19+
});
20+
21+
// 4. Check for JavaScript errors
22+
const jsErrors: string[] = [];
23+
page.on('pageerror', (error) => {
24+
jsErrors.push(error.message);
25+
});
26+
27+
// 5. Check console logs
28+
const consoleLogs: string[] = [];
29+
page.on('console', (msg) => {
30+
consoleLogs.push(`${msg.type()}: ${msg.text()}`);
31+
});
32+
33+
// 6. Check if tags container exists
34+
const tagsContainer = await page.locator('[data-testid="tags-container"]').count();
35+
console.log('Tags container count:', tagsContainer);
36+
37+
// 7. Check if tags list exists
38+
const tagsList = await page.locator('[data-testid="tags-list"]').count();
39+
console.log('Tags list count:', tagsList);
40+
41+
// 8. Check if any tag items exist
42+
const tagItems = await page.locator('[data-testid="tag-item"]').count();
43+
console.log('Tag items count:', tagItems);
44+
45+
// 9. Check the HTML content of tags container
46+
if (tagsContainer > 0) {
47+
const tagsContainerHTML = await page.locator('[data-testid="tags-container"]').innerHTML();
48+
console.log('Tags container HTML:', tagsContainerHTML);
49+
}
50+
51+
// 10. Check for @for loop elements (Angular control flow)
52+
const ngForElements = await page.locator('[ng-for]').count();
53+
console.log('ng-for elements count:', ngForElements);
54+
55+
// 11. Check Angular component state
56+
const componentState = await page.evaluate(() => {
57+
// Try to access Angular component data
58+
const appRoot = document.querySelector('app-root');
59+
if (appRoot) {
60+
return {
61+
hasAppRoot: true,
62+
innerHTML: appRoot.innerHTML.substring(0, 500) + '...'
63+
};
64+
}
65+
return { hasAppRoot: false };
66+
});
67+
console.log('Component state:', componentState);
68+
69+
// 12. Check API calls
70+
console.log('Recent network requests:', allRequests.slice(-10));
71+
console.log('JavaScript errors:', jsErrors);
72+
console.log('Recent console logs:', consoleLogs.slice(-10));
73+
74+
// 13. Check if Supabase client is available
75+
const supabaseAvailable = await page.evaluate(() => {
76+
return typeof (window as any).supabase !== 'undefined';
77+
});
78+
console.log('Supabase client available:', supabaseAvailable);
79+
80+
// 14. Check environment variables
81+
const envCheck = await page.evaluate(() => {
82+
return {
83+
hasSupabaseUrl: typeof process !== 'undefined' && !!process.env?.['SUPABASE_URL'],
84+
hasSupabaseKey: typeof process !== 'undefined' && !!process.env?.['SUPABASE_ANON_KEY']
85+
};
86+
});
87+
console.log('Environment check:', envCheck);
88+
89+
console.log('=== END DEBUG INFO ===\n');
90+
}
91+
92+
export async function waitForAngularToLoad(page: Page, timeout = 10000) {
93+
console.log('Waiting for Angular to load...');
94+
95+
try {
96+
// Wait for Angular to be ready
97+
await page.waitForFunction(() => {
98+
return !!(window as any).ng && document.querySelector('app-root');
99+
}, { timeout });
100+
101+
console.log('Angular loaded successfully');
102+
return true;
103+
} catch (error) {
104+
console.log('Angular failed to load within timeout:', error);
105+
return false;
106+
}
107+
}
108+
109+
export async function waitForApiCall(page: Page, urlPattern: string, timeout = 15000) {
110+
console.log(`Waiting for API call matching: ${urlPattern}`);
111+
112+
return new Promise((resolve, reject) => {
113+
const timer = setTimeout(() => {
114+
reject(new Error(`API call to ${urlPattern} not detected within ${timeout}ms`));
115+
}, timeout);
116+
117+
const requestHandler = (request: any) => {
118+
if (request.url().includes(urlPattern)) {
119+
console.log(`API call detected: ${request.url()}`);
120+
clearTimeout(timer);
121+
page.off('request', requestHandler);
122+
resolve(request);
123+
}
124+
};
125+
126+
page.on('request', requestHandler);
127+
});
128+
}

e2e/tags.spec.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { test, expect } from '@playwright/test';
2+
import { acceptCookies } from './helpers/cookie-consent.helper';
3+
import { waitForAngularToLoad, waitForApiCall } from './helpers/debug.helper';
4+
5+
test.describe('Tags Display and API', () => {
6+
test('should display tags and verify API call', async ({ page }) => {
7+
const apiRequests: Array<{
8+
url: string;
9+
method: string;
10+
headers: Record<string, string>;
11+
status?: number;
12+
}> = [];
13+
14+
page.on('request', (request) => {
15+
const url = request.url();
16+
if (
17+
url.includes('/rest/v1/tags') ||
18+
url.includes('supabase') ||
19+
url.includes('tag')
20+
) {
21+
apiRequests.push({
22+
url,
23+
method: request.method(),
24+
headers: request.headers(),
25+
});
26+
}
27+
});
28+
29+
page.on('response', (response) => {
30+
const url = response.url();
31+
if (url.includes('/rest/v1/tags')) {
32+
const existingRequest = apiRequests.find((req) => req.url === url);
33+
if (existingRequest) {
34+
existingRequest.status = response.status();
35+
}
36+
}
37+
});
38+
39+
await page.goto('/', { waitUntil: 'networkidle' });
40+
await acceptCookies(page);
41+
await waitForAngularToLoad(page, 500);
42+
await page.waitForSelector('[data-testid="tags-container"]', {
43+
timeout: 500,
44+
});
45+
46+
try {
47+
await waitForApiCall(page, '/rest/v1/tags', 300);
48+
} catch (error) {
49+
// Continue test even if API call detection fails
50+
}
51+
52+
await page.waitForTimeout(1000);
53+
await page.waitForSelector('[data-testid="tag-item"]', { timeout: 500 });
54+
55+
const tagsContainer = page.locator('[data-testid="tags-container"]');
56+
await expect(tagsContainer).toBeVisible();
57+
58+
const expectedTags = [
59+
{ name: 'Angular', color: '#DD0031', icon: 'angular.svg' },
60+
{ name: 'TypeScript', color: '#007ACC', icon: 'typescript.svg' },
61+
{ name: 'JavaScript', color: '#F7DF1E', icon: 'javascript.svg' },
62+
];
63+
64+
for (const tag of expectedTags) {
65+
const tagItem = page.locator(
66+
`[data-testid="tag-item"][data-tag-name="${tag.name}"]`,
67+
);
68+
await expect(tagItem).toBeVisible();
69+
70+
const tagName = tagItem.locator('[data-testid="tag-name"]');
71+
await expect(tagName).toHaveText(tag.name);
72+
}
73+
74+
expect(apiRequests.length).toBeGreaterThan(0);
75+
});
76+
});

e2e/tsconfig.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../tsconfig.json",
3+
"include": ["./**/*.ts"]
4+
}

0 commit comments

Comments
 (0)