Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0693945
Refactor admin API service to improve code readability and maintainab…
Lukecsharpwalker Jun 10, 2025
24293c9
Add Playwright configuration and initial test for cookie consent func…
Lukecsharpwalker Jun 11, 2025
1d42374
Add GitHub Actions workflow for E2E tests with local Supabase setup
Lukecsharpwalker Jun 11, 2025
4b3c986
Refactor E2E test workflow to use local Supabase instance and improve…
Lukecsharpwalker Jun 11, 2025
25a52ff
Improve Supabase CLI installation process in E2E tests workflow
Lukecsharpwalker Jun 11, 2025
fec9cdf
Enhance E2E test workflow by cleaning npm cache and adjusting install…
Lukecsharpwalker Jun 11, 2025
e5d382c
Add cookie consent helper and update tests to handle cookie consent f…
Lukecsharpwalker Jun 11, 2025
e8311aa
Add API connectivity tests and debugging helpers for improved test re…
Lukecsharpwalker Jun 11, 2025
ffafe02
Refactor E2E test workflow to improve clarity and efficiency with upd…
Lukecsharpwalker Jun 11, 2025
763754b
Remove unused Postgres service configuration from E2E test workflow
Lukecsharpwalker Jun 11, 2025
8996eb6
Update Supabase CLI version in E2E test workflow to 2.24.3
Lukecsharpwalker Jun 11, 2025
ba2ce4f
Refactor E2E test workflow for improved clarity and efficiency, updat…
Lukecsharpwalker Jun 11, 2025
82831bb
Refactor E2E test workflow for improved clarity and efficiency, updat…
Lukecsharpwalker Jun 11, 2025
8d7781d
Update local environment configuration and streamline E2E test workfl…
Lukecsharpwalker Jun 11, 2025
82e153e
Update local environment configuration and streamline E2E test workfl…
Lukecsharpwalker Jun 11, 2025
34da2a4
Update E2E local command to use Angular CLI configuration
Lukecsharpwalker Jun 11, 2025
b584772
Add explicit installation of Rollup helper in E2E test workflow
Lukecsharpwalker Jun 11, 2025
1f58b16
Streamline E2E test workflow by removing explicit Rollup installation…
Lukecsharpwalker Jun 11, 2025
4d9bcee
Streamline E2E test workflow by removing explicit Rollup installation…
Lukecsharpwalker Jun 11, 2025
5ed1c54
Enhance E2E test workflow with improved step descriptions and conditi…
Lukecsharpwalker Jun 11, 2025
40faeb7
Increase timeout for Supabase wait step in E2E test workflow
Lukecsharpwalker Jun 11, 2025
52d0220
Rollback E2E build
Lukecsharpwalker Jun 11, 2025
c8623b7
Rollback E2E build
Lukecsharpwalker Jun 11, 2025
ffad979
images refactor
Lukecsharpwalker Jun 11, 2025
823f129
tags test corrected
Lukecsharpwalker Jun 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: E2E Tests with Local Supabase

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]

jobs:
e2e:
if: "!contains(github.event.head_commit.message, 'E2E')"
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: |
node - <<'NODE'
const v = require('./package.json').devDependencies.rollup ?? 'latest';
console.log('ℹ Installing native Rollup helper for', v);
NODE
- run: npm install --no-save @rollup/rollup-linux-x64-gnu@$(node -p "require('./package.json').devDependencies.rollup || '4'")

- uses: supabase/setup-cli@v1
with: { version: 2.24.3 }

- name: Start Supabase
env: { SUPABASE_TELEMETRY_DISABLED: "1" }
run: supabase start &

- name: Wait for Supabase (≤180 s)
run: npx --yes wait-on tcp:127.0.0.1:54321 tcp:127.0.0.1:54322 --timeout 180000

- run: echo "SUPABASE_URL=http://127.0.0.1:54321" >> $GITHUB_ENV
- run: |
echo "SUPABASE_ANON_KEY=$(supabase status -o env | grep SUPABASE_ANON_KEY | cut -d= -f2)" >> $GITHUB_ENV

- run: npx playwright install --with-deps
- run: npm run e2e:local
env: { CI: "true" }

- uses: actions/upload-artifact@v4
if: always()
with: { name: playwright-report, path: playwright-report }
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,8 @@ Thumbs.db
.runtimeconfig.json
adminSdkConf.json


# Playwright
/test-results/
/playwright-report/
/playwright/.cache/
14 changes: 14 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@
],
"scripts": []
}
},
"e2e": {
"builder": "playwright-ng-schematics:playwright",
"options": {
"devServerTarget": "angularblogapp:serve"
},
"configurations": {
"production": {
"devServerTarget": "angularblogapp:serve:production"
},
"local": {
"devServerTarget": "angularblogapp:serve:local"
}
}
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions e2e/example.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { test, expect } from '@playwright/test';
import { acceptCookies } from './helpers/cookie-consent.helper';

test('has title', async ({ page }) => {
await page.goto('/');

// Handle cookie consent using helper
await acceptCookies(page);

// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/AngularBlogApp/);
});
16 changes: 16 additions & 0 deletions e2e/helpers/cookie-consent.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Page, expect } from '@playwright/test';

export async function acceptCookies(page: Page): Promise<void> {
const cookieConsentDialog = page
.getByLabel('Cookie Consent')
.locator('div')
.filter({ hasText: 'Cookie Consent Consent' })
.nth(1);

await expect(cookieConsentDialog).toBeVisible();

const allowAllButton = page.getByRole('button', { name: 'Allow All' });
await allowAllButton.click();

await expect(cookieConsentDialog).not.toBeVisible();
}
128 changes: 128 additions & 0 deletions e2e/helpers/debug.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Page } from '@playwright/test';

export async function debugPageState(page: Page, testName: string) {
console.log(`=== DEBUG INFO FOR: ${testName} ===`);

// 1. Check current URL
console.log('Current URL:', page.url());

// 2. Check if Angular app is loaded
const angularLoaded = await page.evaluate(() => {
return !!(window as any).ng;
});
console.log('Angular loaded:', angularLoaded);

// 3. Check network requests
const allRequests: string[] = [];
page.on('request', (request) => {
allRequests.push(`${request.method()} ${request.url()}`);
});

// 4. Check for JavaScript errors
const jsErrors: string[] = [];
page.on('pageerror', (error) => {
jsErrors.push(error.message);
});

// 5. Check console logs
const consoleLogs: string[] = [];
page.on('console', (msg) => {
consoleLogs.push(`${msg.type()}: ${msg.text()}`);
});

// 6. Check if tags container exists
const tagsContainer = await page.locator('[data-testid="tags-container"]').count();
console.log('Tags container count:', tagsContainer);

// 7. Check if tags list exists
const tagsList = await page.locator('[data-testid="tags-list"]').count();
console.log('Tags list count:', tagsList);

// 8. Check if any tag items exist
const tagItems = await page.locator('[data-testid="tag-item"]').count();
console.log('Tag items count:', tagItems);

// 9. Check the HTML content of tags container
if (tagsContainer > 0) {
const tagsContainerHTML = await page.locator('[data-testid="tags-container"]').innerHTML();
console.log('Tags container HTML:', tagsContainerHTML);
}

// 10. Check for @for loop elements (Angular control flow)
const ngForElements = await page.locator('[ng-for]').count();
console.log('ng-for elements count:', ngForElements);

// 11. Check Angular component state
const componentState = await page.evaluate(() => {
// Try to access Angular component data
const appRoot = document.querySelector('app-root');
if (appRoot) {
return {
hasAppRoot: true,
innerHTML: appRoot.innerHTML.substring(0, 500) + '...'
};
}
return { hasAppRoot: false };
});
console.log('Component state:', componentState);

// 12. Check API calls
console.log('Recent network requests:', allRequests.slice(-10));
console.log('JavaScript errors:', jsErrors);
console.log('Recent console logs:', consoleLogs.slice(-10));

// 13. Check if Supabase client is available
const supabaseAvailable = await page.evaluate(() => {
return typeof (window as any).supabase !== 'undefined';
});
console.log('Supabase client available:', supabaseAvailable);

// 14. Check environment variables
const envCheck = await page.evaluate(() => {
return {
hasSupabaseUrl: typeof process !== 'undefined' && !!process.env?.['SUPABASE_URL'],
hasSupabaseKey: typeof process !== 'undefined' && !!process.env?.['SUPABASE_ANON_KEY']
};
});
console.log('Environment check:', envCheck);

console.log('=== END DEBUG INFO ===\n');
}

export async function waitForAngularToLoad(page: Page, timeout = 10000) {
console.log('Waiting for Angular to load...');

try {
// Wait for Angular to be ready
await page.waitForFunction(() => {
return !!(window as any).ng && document.querySelector('app-root');
}, { timeout });

console.log('Angular loaded successfully');
return true;
} catch (error) {
console.log('Angular failed to load within timeout:', error);
return false;
}
}

export async function waitForApiCall(page: Page, urlPattern: string, timeout = 15000) {
console.log(`Waiting for API call matching: ${urlPattern}`);

return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`API call to ${urlPattern} not detected within ${timeout}ms`));
}, timeout);

const requestHandler = (request: any) => {
if (request.url().includes(urlPattern)) {
console.log(`API call detected: ${request.url()}`);
clearTimeout(timer);
page.off('request', requestHandler);
resolve(request);
}
};

page.on('request', requestHandler);
});
}
76 changes: 76 additions & 0 deletions e2e/tags.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { test, expect } from '@playwright/test';
import { acceptCookies } from './helpers/cookie-consent.helper';
import { waitForAngularToLoad, waitForApiCall } from './helpers/debug.helper';

test.describe('Tags Display and API', () => {
test('should display tags and verify API call', async ({ page }) => {
const apiRequests: Array<{
url: string;
method: string;
headers: Record<string, string>;
status?: number;
}> = [];

page.on('request', (request) => {
const url = request.url();
if (
url.includes('/rest/v1/tags') ||
url.includes('supabase') ||
url.includes('tag')
) {
apiRequests.push({
url,
method: request.method(),
headers: request.headers(),
});
}
});

page.on('response', (response) => {
const url = response.url();
if (url.includes('/rest/v1/tags')) {
const existingRequest = apiRequests.find((req) => req.url === url);
if (existingRequest) {
existingRequest.status = response.status();
}
}
});

await page.goto('/', { waitUntil: 'networkidle' });
await acceptCookies(page);
await waitForAngularToLoad(page, 500);
await page.waitForSelector('[data-testid="tags-container"]', {
timeout: 500,
});

try {
await waitForApiCall(page, '/rest/v1/tags', 300);
} catch (error) {
// Continue test even if API call detection fails
}

await page.waitForTimeout(1000);
await page.waitForSelector('[data-testid="tag-item"]', { timeout: 500 });

const tagsContainer = page.locator('[data-testid="tags-container"]');
await expect(tagsContainer).toBeVisible();

const expectedTags = [
{ name: 'Angular', color: '#DD0031', icon: 'angular.svg' },
{ name: 'TypeScript', color: '#007ACC', icon: 'typescript.svg' },
{ name: 'JavaScript', color: '#F7DF1E', icon: 'javascript.svg' },
];

for (const tag of expectedTags) {
const tagItem = page.locator(
`[data-testid="tag-item"][data-tag-name="${tag.name}"]`,
);
await expect(tagItem).toBeVisible();

const tagName = tagItem.locator('[data-testid="tag-name"]');
await expect(tagName).toHaveText(tag.name);
}

expect(apiRequests.length).toBeGreaterThan(0);
});
});
4 changes: 4 additions & 0 deletions e2e/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["./**/*.ts"]
}
Loading