Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
53 changes: 53 additions & 0 deletions packages/plugin-e2e/src/fixtures/isFeatureToggleEnabled.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Page } from '@playwright/test';
import { describe, it, expect, vi } from 'vitest';

import { isLegacyFeatureEnabled } from './isFeatureToggleEnabled';

/**
* Stands in for a Playwright Page. waitForFunction resolves with a handle whose jsonValue()
* returns the toggle map, or rejects to simulate the timeout.
*/
function createMockPage(behaviour: { toggles?: Record<string, boolean>; timesOut?: boolean }) {
return {
waitForFunction: vi.fn().mockImplementation(() => {
if (behaviour.timesOut) {
return Promise.reject(new Error('Timeout 5000ms exceeded'));
}
return Promise.resolve({ jsonValue: () => Promise.resolve(behaviour.toggles) });
}),
} as unknown as Page;
}

describe('isLegacyFeatureEnabled', () => {
it('returns true for an enabled toggle', async () => {
const page = createMockPage({ toggles: { dashboardNewLayouts: true } });
await expect(isLegacyFeatureEnabled(page, 'dashboardNewLayouts')).resolves.toBe(true);
});

it('returns false for a toggle that is absent from the map', async () => {
const page = createMockPage({ toggles: { somethingElse: true } });
await expect(isLegacyFeatureEnabled(page, 'dashboardNewLayouts')).resolves.toBe(false);
});

it('waits for the toggle map rather than reading it once', async () => {
const page = createMockPage({ toggles: { dashboardNewLayouts: true } });
await isLegacyFeatureEnabled(page, 'dashboardNewLayouts');
expect(page.waitForFunction).toHaveBeenCalledTimes(1);
});

// Falling back to an empty map here would be indistinguishable from every toggle being
// disabled, so callers would silently take a legacy branch on an instance where the toggle is
// on. The timeout must stay loud.
it('rethrows when boot data never arrives rather than reporting every toggle as disabled', async () => {
const page = createMockPage({ timesOut: true });
await expect(isLegacyFeatureEnabled(page, 'dashboardNewLayouts')).rejects.toThrow(
/featureToggles was not available within 5000ms/
);
});

it('preserves the underlying timeout as the error cause', async () => {
const page = createMockPage({ timesOut: true });
const error = await isLegacyFeatureEnabled(page, 'dashboardNewLayouts').catch((e: unknown) => e);
expect((error as Error).cause).toEqual(new Error('Timeout 5000ms exceeded'));
});
});
35 changes: 33 additions & 2 deletions packages/plugin-e2e/src/fixtures/isFeatureToggleEnabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,48 @@ import { PlaywrightArgs } from '../types';

type FeatureToggleFixture = TestFixture<<T = object>(featureToggle: keyof T) => Promise<boolean>, PlaywrightArgs>;

const FEATURE_TOGGLES_TIMEOUT = 5000;

/**
* Reads the feature toggle map from boot data, waiting for it to arrive.
*
* Grafana can render the app shell before it has merged its boot data, in which case
* `settings.featureToggles` is briefly absent even though the Window type declares it as always
* present. Reading it once then throws `Cannot read properties of undefined`.
*
* Waiting removes that race. It deliberately does not fall back to an empty map when the wait
* times out, because a missing map is indistinguishable from every toggle being disabled: a
* caller would silently take its legacy branch on an instance where the toggle is enabled. A
* loud failure is the lesser evil, so the timeout is rethrown with the context needed to act
* on it.
*/
const readFeatureToggles = async <T>(page: Page): Promise<T> => {
try {
const featureToggles = await page.waitForFunction(
() => window.grafanaBootData?.settings?.featureToggles ?? null,
undefined,
{ timeout: FEATURE_TOGGLES_TIMEOUT }
);
return (await featureToggles.jsonValue()) as T;
} catch (error) {
throw new Error(
`@grafana/plugin-e2e: window.grafanaBootData.settings.featureToggles was not available within ${FEATURE_TOGGLES_TIMEOUT}ms`,
{ cause: error }
);
}
};

export const isLegacyFeatureToggleEnabled: FeatureToggleFixture = async ({ page }, use) => {
await use(async <T = object>(featureToggle: keyof T) => {
const featureToggles: T = await page.evaluate('window.grafanaBootData.settings.featureToggles');
const featureToggles = await readFeatureToggles<T>(page);
return Boolean(featureToggles[featureToggle]);
});
};

export const isFeatureToggleEnabled: FeatureToggleFixture = isLegacyFeatureToggleEnabled;

export const isLegacyFeatureEnabled = async (page: Page, featureToggle: string) => {
const featureToggles: Record<string, string> = await page.evaluate('window.grafanaBootData.settings.featureToggles');
const featureToggles = await readFeatureToggles<Record<string, boolean>>(page);
return Boolean(featureToggles[featureToggle]);
};

Expand Down
Loading