mirror of
https://github.com/apache/superset.git
synced 2026-05-25 09:45:18 +00:00
Skeleton commit for the subdirectory deployment refactor. Adds the test framework and one example test per layer; the helpers themselves are stubbed so the suite is meaningfully red until the green commit lands. Frameworks - spec/helpers/withApplicationRoot.ts: fixture that rewrites #app data and resets the module cache so getBootstrapData() returns the requested application root inside the callback. Replaces the inline ritual that pathUtils.test.ts currently repeats per test. - spec/helpers/sourceTreeScanner.ts: line-by-line regex scanner over the source tree with allow-list support. Backs the static-invariant tests in Layer 2 with workspace-relative file:line locations on failure. Stubs - src/utils/navigationUtils.ts: openInNewTab, redirect, redirectReplace, getShareableUrl, AppLink. Each throws a "not implemented" error with a doc comment describing the channel rule it enforces. Existing navigateTo / navigateWithState are kept untouched and called out as legacy multi-mode helpers scheduled for replacement. - packages/superset-ui-core/src/connection/normalizeBackendUrls.ts: conservative URL field normaliser. Ships the curated NORMALIZED_URL_FIELDS set (initially empty pending per-endpoint audit) and a documented NORMALIZER_EXCLUSIONS list explaining why bug_report_url, thumbnail_url, user_login_url, etc. are deliberately not normalised. Layered tests (one example each; full suite expands per layer in subsequent commits on this PR) - Layer 1 unit: navigationUtils.test.ts exercises openInNewTab under empty / single / nested application roots, plus absolute-URL and mailto passthrough. Red until the helper is implemented. - Layer 2 invariant: navigationUtils.invariants.test.ts asserts that ensureAppRoot / makeUrl are not imported outside navigationUtils.ts. Allow-list seeded with the 19 current call sites so the test is GREEN on day one; migration commits delete entries from the list. - Layer 3 normaliser: normalizeBackendUrls.test.ts pairs a positive strip case with negative passthrough cases (non-allow-listed field, absolute URL, similar-but-different prefix segment, empty root). Red until the normaliser is implemented. - Layer 4 contract: SupersetClientAppRootContract.test.ts pins the channel-2 invariant (root applied exactly once, never doubled). Documents the double-prefix symptom in a regression assertion. - Layer 5 regression: SliceHeaderControls.subdirectory.test.tsx asserts Cmd-click "Edit chart" opens a prefixed URL when the app is deployed under a subdirectory. Red until index.tsx:266 is migrated to openInNewTab. Strategy: each subsequent commit on this PR fans out one layer to its full coverage and migrates the corresponding call sites, shrinking the Layer 2 allow-list in lockstep. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
92 lines
3.5 KiB
TypeScript
92 lines
3.5 KiB
TypeScript
/**
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
/**
|
|
* Test fixture for subdirectory-deployment scenarios.
|
|
*
|
|
* Bootstrap data in Superset is read once per module load via
|
|
* `getBootstrapData()` and cached. Tests that exercise URL generation under a
|
|
* non-empty `application_root` therefore need to rewrite the `#app` element
|
|
* and force the relevant modules to re-import so the cache is rebuilt.
|
|
*
|
|
* `withApplicationRoot` centralises that ritual. Usage:
|
|
*
|
|
* import { withApplicationRoot } from 'spec/helpers/withApplicationRoot';
|
|
*
|
|
* test('redirects to prefixed root under subdirectory deployment', async () => {
|
|
* await withApplicationRoot('/superset/', async () => {
|
|
* const { redirect } = await import('src/utils/navigationUtils');
|
|
* redirect('/');
|
|
* expect(window.location.href).toBe('/superset/');
|
|
* });
|
|
* });
|
|
*
|
|
* The callback receives a freshly-reset module registry, so any imports inside
|
|
* it observe the configured root. After the callback finishes (or throws), the
|
|
* fixture restores the previous `<div id="app">` markup and resets modules
|
|
* again, leaving the global state clean for the next test.
|
|
*
|
|
* Pass `''` (the default) to simulate a deployment with no application root.
|
|
*/
|
|
export async function withApplicationRoot<T>(
|
|
applicationRoot: string,
|
|
callback: () => Promise<T> | T,
|
|
): Promise<T> {
|
|
const previousBody = document.body.innerHTML;
|
|
|
|
try {
|
|
const bootstrapData = {
|
|
common: { application_root: applicationRoot },
|
|
};
|
|
document.body.innerHTML = `<div id="app" data-bootstrap='${JSON.stringify(bootstrapData)}'></div>`;
|
|
jest.resetModules();
|
|
|
|
// Touch getBootstrapData first so the cached value reflects the new DOM.
|
|
await import('src/utils/getBootstrapData');
|
|
|
|
return await callback();
|
|
} finally {
|
|
document.body.innerHTML = previousBody;
|
|
jest.resetModules();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convenience wrapper that runs the same assertion under multiple application
|
|
* roots. Use when the assertion text doesn't depend on the prefix.
|
|
*
|
|
* applicationRootScenarios([
|
|
* { root: '', expected: '/sqllab' },
|
|
* { root: '/superset/', expected: '/superset/sqllab' },
|
|
* { root: '/a/b/c/', expected: '/a/b/c/sqllab' },
|
|
* ], async ({ expected }) => {
|
|
* const { ensureAppRoot } = await import('src/utils/pathUtils');
|
|
* expect(ensureAppRoot('/sqllab')).toBe(expected);
|
|
* });
|
|
*/
|
|
export async function applicationRootScenarios<S extends { root: string }>(
|
|
scenarios: S[],
|
|
body: (scenario: S) => Promise<void> | void,
|
|
): Promise<void> {
|
|
for (const scenario of scenarios) {
|
|
// eslint-disable-next-line no-await-in-loop -- intentional: scenarios share document state.
|
|
await withApplicationRoot(scenario.root, () => body(scenario));
|
|
}
|
|
}
|