Skip to content

Commit d5f5546

Browse files
committed
feat(feature-flags): add e2e tests and fix startup table creation
- Add Playwright e2e smoke tests (page load, table visibility) - Add Playwright e2e flow tests (toggle flag, override CRUD, check endpoint, overrides dialog) - Add page object for FeatureFlags manage page - Fix FeatureFlagSyncService to create module tables at startup via CreateTablesAsync — prevents crash when module is added to an existing database that only has host tables https://claude.ai/code/session_01Cfi9aG39WHwLr2cmsmPAnK
1 parent 6c48866 commit d5f5546

4 files changed

Lines changed: 196 additions & 0 deletions

File tree

‎modules/FeatureFlags/src/SimpleModule.FeatureFlags/FeatureFlagSyncService.cs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
using System.Data.Common;
12
using Microsoft.EntityFrameworkCore;
3+
using Microsoft.EntityFrameworkCore.Infrastructure;
4+
using Microsoft.EntityFrameworkCore.Storage;
25
using Microsoft.Extensions.DependencyInjection;
36
using Microsoft.Extensions.Hosting;
47
using Microsoft.Extensions.Logging;
@@ -18,6 +21,19 @@ public async Task StartAsync(CancellationToken cancellationToken)
1821
using var scope = serviceProvider.CreateScope();
1922
var db = scope.ServiceProvider.GetRequiredService<FeatureFlagsDbContext>();
2023

24+
// Ensure module tables exist. EnsureCreated is a no-op when the database
25+
// file already exists (created by the Host DbContext), so we call
26+
// CreateTables directly and ignore the error if tables already exist.
27+
try
28+
{
29+
var creator = db.Database.GetService<IRelationalDatabaseCreator>();
30+
await creator.CreateTablesAsync(cancellationToken);
31+
}
32+
catch (DbException)
33+
{
34+
// Tables already exist — expected in normal operation
35+
}
36+
2137
var definitions = registry.GetAllDefinitions();
2238
var existingFlags = await db.FeatureFlags.ToListAsync(cancellationToken);
2339
var existingNames = existingFlags.ToDictionary(f => f.Name);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { Page } from '@playwright/test';
2+
3+
export class FeatureFlagsManagePage {
4+
constructor(private page: Page) {}
5+
6+
async goto() {
7+
await this.page.goto('/feature-flags');
8+
}
9+
10+
get heading() {
11+
return this.page.getByRole('heading', { name: /feature flags/i });
12+
}
13+
14+
get flagTable() {
15+
return this.page.getByRole('table');
16+
}
17+
18+
get flagRows() {
19+
return this.page.getByRole('row');
20+
}
21+
22+
flagRow(name: string) {
23+
return this.page.getByRole('row', { name: new RegExp(name, 'i') });
24+
}
25+
26+
flagToggle(name: string) {
27+
return this.flagRow(name).getByRole('switch');
28+
}
29+
30+
overridesButton(name: string) {
31+
return this.flagRow(name).getByRole('button', { name: /overrides/i });
32+
}
33+
34+
get overrideDialog() {
35+
return this.page.getByRole('dialog');
36+
}
37+
38+
get addOverrideButton() {
39+
return this.overrideDialog.getByRole('button', { name: /add override/i });
40+
}
41+
42+
get overrideValueInput() {
43+
return this.overrideDialog.getByLabel(/value/i);
44+
}
45+
46+
get deprecatedSection() {
47+
return this.page.getByText(/deprecated/i);
48+
}
49+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { expect, test } from '../../fixtures/base';
2+
import { FeatureFlagsManagePage } from '../../pages/feature-flags/manage.page';
3+
4+
test.describe('Feature Flags CRUD flows', () => {
5+
test('toggle a feature flag and verify state persists', async ({ page, request }) => {
6+
const manage = new FeatureFlagsManagePage(page);
7+
8+
// API: get all flags to find one to toggle
9+
const flagsRes = await request.get('/api/feature-flags');
10+
expect(flagsRes.ok()).toBeTruthy();
11+
const flags = await flagsRes.json();
12+
expect(flags.length).toBeGreaterThan(0);
13+
14+
const flag = flags[0];
15+
const originalState = flag.isEnabled;
16+
17+
// API: toggle the flag
18+
const toggleRes = await request.put(`/api/feature-flags/${encodeURIComponent(flag.name)}`, {
19+
data: { isEnabled: !originalState },
20+
});
21+
expect(toggleRes.ok()).toBeTruthy();
22+
23+
// API: verify the state changed
24+
const afterToggleRes = await request.get('/api/feature-flags');
25+
const afterToggle = await afterToggleRes.json();
26+
const toggled = afterToggle.find((f: { name: string }) => f.name === flag.name);
27+
expect(toggled.isEnabled).toBe(!originalState);
28+
29+
// UI: verify the state on the manage page
30+
await manage.goto();
31+
await expect(manage.flagRow(flag.name)).toBeVisible();
32+
33+
// API: restore original state
34+
await request.put(`/api/feature-flags/${encodeURIComponent(flag.name)}`, {
35+
data: { isEnabled: originalState },
36+
});
37+
});
38+
39+
test('add and remove an override via API', async ({ request }) => {
40+
// API: get a flag to work with
41+
const flagsRes = await request.get('/api/feature-flags');
42+
const flags = await flagsRes.json();
43+
expect(flags.length).toBeGreaterThan(0);
44+
const flag = flags[0];
45+
46+
// API: add a user override
47+
const addRes = await request.post(
48+
`/api/feature-flags/${encodeURIComponent(flag.name)}/overrides`,
49+
{
50+
data: {
51+
overrideType: 0, // User
52+
overrideValue: 'e2e-test-user',
53+
isEnabled: true,
54+
},
55+
},
56+
);
57+
expect(addRes.ok()).toBeTruthy();
58+
const override = await addRes.json();
59+
expect(override.flagName).toBe(flag.name);
60+
expect(override.overrideValue).toBe('e2e-test-user');
61+
62+
// API: verify override appears in list
63+
const overridesRes = await request.get(
64+
`/api/feature-flags/${encodeURIComponent(flag.name)}/overrides`,
65+
);
66+
expect(overridesRes.ok()).toBeTruthy();
67+
const overrides = await overridesRes.json();
68+
expect(
69+
overrides.some((o: { overrideValue: string }) => o.overrideValue === 'e2e-test-user'),
70+
).toBeTruthy();
71+
72+
// API: delete the override
73+
const deleteRes = await request.delete(`/api/feature-flags/overrides/${override.id}`);
74+
expect(deleteRes.ok()).toBeTruthy();
75+
76+
// API: verify override is gone
77+
const afterDeleteRes = await request.get(
78+
`/api/feature-flags/${encodeURIComponent(flag.name)}/overrides`,
79+
);
80+
const afterDelete = await afterDeleteRes.json();
81+
expect(
82+
afterDelete.some((o: { overrideValue: string }) => o.overrideValue === 'e2e-test-user'),
83+
).toBeFalsy();
84+
});
85+
86+
test('check endpoint returns flag state for current user', async ({ request }) => {
87+
// API: get a flag
88+
const flagsRes = await request.get('/api/feature-flags');
89+
const flags = await flagsRes.json();
90+
expect(flags.length).toBeGreaterThan(0);
91+
const flag = flags[0];
92+
93+
// API: check endpoint
94+
const checkRes = await request.get(`/api/feature-flags/check/${encodeURIComponent(flag.name)}`);
95+
expect(checkRes.ok()).toBeTruthy();
96+
const result = await checkRes.json();
97+
expect(typeof result.isEnabled).toBe('boolean');
98+
});
99+
100+
test('overrides dialog opens from manage page', async ({ page, request }) => {
101+
const manage = new FeatureFlagsManagePage(page);
102+
103+
// API: get a flag name
104+
const flagsRes = await request.get('/api/feature-flags');
105+
const flags = await flagsRes.json();
106+
expect(flags.length).toBeGreaterThan(0);
107+
const flagName = flags[0].name;
108+
109+
// UI: navigate and open overrides dialog
110+
await manage.goto();
111+
await manage.overridesButton(flagName).click();
112+
await expect(manage.overrideDialog).toBeVisible();
113+
await expect(manage.addOverrideButton).toBeVisible();
114+
});
115+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { expect, test } from '../../fixtures/base';
2+
import { FeatureFlagsManagePage } from '../../pages/feature-flags/manage.page';
3+
4+
test.describe('Feature Flags pages', () => {
5+
test('manage page loads', async ({ page }) => {
6+
const manage = new FeatureFlagsManagePage(page);
7+
await manage.goto();
8+
await expect(manage.heading).toBeVisible();
9+
});
10+
11+
test('manage page shows flag table', async ({ page }) => {
12+
const manage = new FeatureFlagsManagePage(page);
13+
await manage.goto();
14+
await expect(manage.flagTable).toBeVisible();
15+
});
16+
});

0 commit comments

Comments
 (0)