diff --git a/.github/workflows/playwright-nightly.yml b/.github/workflows/playwright-nightly.yml index f3efaa89b9..f1bef46f7a 100644 --- a/.github/workflows/playwright-nightly.yml +++ b/.github/workflows/playwright-nightly.yml @@ -37,6 +37,10 @@ jobs: playwright-tests: needs: resolve-versions timeout-minutes: 60 + # trial: fetch e2e-selectors from the running Grafana at runtime instead of the bundled dep. + # grafana-dev serves the file (exercises the fetch); latest stable 404s (exercises the fallback) + env: + PLUGIN_E2E_RUNTIME_SELECTORS: 'true' strategy: fail-fast: false matrix: diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 23a15740a7..17322601b6 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -33,6 +33,9 @@ jobs: playwright-tests: needs: resolve-versions timeout-minutes: 60 + # trial: fetch e2e-selectors from the running Grafana at runtime instead of the bundled dep + env: + PLUGIN_E2E_RUNTIME_SELECTORS: 'true' strategy: fail-fast: false matrix: diff --git a/packages/plugin-e2e/src/fixtures/selectors.ts b/packages/plugin-e2e/src/fixtures/selectors.ts index cac81cd4c1..4744a5ef5b 100644 --- a/packages/plugin-e2e/src/fixtures/selectors.ts +++ b/packages/plugin-e2e/src/fixtures/selectors.ts @@ -1,16 +1,110 @@ -import { TestFixture } from '@playwright/test'; +import { APIRequestContext, TestFixture } from '@playwright/test'; +import { + resolveSelectors, + versionedComponents as bundledVersionedComponents, + versionedPages as bundledVersionedPages, +} from '@grafana/e2e-selectors'; import { E2ESelectorGroups, PlaywrightArgs } from '../types'; -import { resolveSelectors, versionedComponents, versionedPages } from '@grafana/e2e-selectors'; import { versionedConstants } from '../selectors/versionedConstants'; import { versionedAPIs } from '../selectors/versionedAPIs'; type SelectorFixture = TestFixture; -export const selectors: SelectorFixture = async ({ grafanaVersion }, use) => { - await use({ - components: resolveSelectors(versionedComponents, grafanaVersion), - pages: resolveSelectors(versionedPages, grafanaVersion), +// served by grafana/grafana from the @grafana/e2e-selectors build (see PLUGIN_E2E_RUNTIME_SELECTORS) +const SELECTORS_URL = '/public/e2e-selectors.js'; + +// only the components/pages data comes from Grafana; constants/apis are plugin-e2e's own +type RuntimeSelectors = { + versionedComponents: typeof bundledVersionedComponents; + versionedPages: typeof bundledVersionedPages; +}; + +// per-worker cache keyed by grafanaVersion so concurrent fixtures share one in-flight fetch +const selectorsCache = new Map>(); + +// evaluate the fetched CJS bundle with a require shim that allows only 'semver' +function evaluateBundle(src: string): RuntimeSelectors { + const shimRequire = (id: string) => { + if (id !== 'semver') { + throw new Error(`@grafana/plugin-e2e: disallowed require('${id}') in e2e-selectors bundle`); + } + return require('semver'); + }; + const module: { exports: Partial } = { exports: {} }; + new Function('require', 'module', 'exports', src)(shimRequire, module, module.exports); + + const { versionedComponents, versionedPages } = module.exports; + if (!versionedComponents || !versionedPages) { + throw new Error('@grafana/plugin-e2e: e2e-selectors bundle is missing expected exports'); + } + return { versionedComponents, versionedPages }; +} + +function buildGroups(runtime: RuntimeSelectors, grafanaVersion: string): E2ESelectorGroups { + return { + components: resolveSelectors(runtime.versionedComponents, grafanaVersion), + pages: resolveSelectors(runtime.versionedPages, grafanaVersion), constants: resolveSelectors(versionedConstants, grafanaVersion), apis: resolveSelectors(versionedAPIs, grafanaVersion), - }); + }; +} + +// fall back to the selectors bundled with the installed @grafana/plugin-e2e release +function bundledGroups(grafanaVersion: string): E2ESelectorGroups { + return buildGroups( + { versionedComponents: bundledVersionedComponents, versionedPages: bundledVersionedPages }, + grafanaVersion + ); +} + +async function fetchRuntimeSelectors(request: APIRequestContext, grafanaVersion: string): Promise { + let response; + try { + response = await request.get(SELECTORS_URL); + } catch (error) { + console.warn( + `@grafana/plugin-e2e: failed to fetch ${SELECTORS_URL}, falling back to bundled @grafana/e2e-selectors.`, + error + ); + return bundledGroups(grafanaVersion); + } + + // 404 -> Grafana predates the feature; expected on older images, fall back quietly + if (response.status() === 404) { + return bundledGroups(grafanaVersion); + } + + // any other non-OK status (5xx etc.) -> loud fallback + if (!response.ok()) { + console.warn( + `@grafana/plugin-e2e: ${SELECTORS_URL} returned ${response.status()}, falling back to bundled @grafana/e2e-selectors.` + ); + return bundledGroups(grafanaVersion); + } + + try { + const src = await response.text(); + return buildGroups(evaluateBundle(src), grafanaVersion); + } catch (error) { + console.warn( + `@grafana/plugin-e2e: failed to evaluate ${SELECTORS_URL}, falling back to bundled @grafana/e2e-selectors.`, + error + ); + return bundledGroups(grafanaVersion); + } +} + +export const selectors: SelectorFixture = async ({ grafanaVersion, request }, use) => { + // opt-in during the trial period; when off, behave exactly as before (bundled dependency) + if (process.env.PLUGIN_E2E_RUNTIME_SELECTORS !== 'true') { + await use(bundledGroups(grafanaVersion)); + return; + } + + let groups = selectorsCache.get(grafanaVersion); + if (!groups) { + groups = fetchRuntimeSelectors(request, grafanaVersion); + selectorsCache.set(grafanaVersion, groups); + } + await use(await groups); };