fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

View File

@@ -0,0 +1,16 @@
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* Licensed 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.
*/
/**
* COMPAT: update from the old TestDefn format (array of `expectations` per
* definition) to the new format (single `expectations` per def), doing our best
* generating some unique IDs.
* TODO: remove in Lighthouse 9+ once PubAds (and others?) are updated.
* @see https://github.com/GoogleChrome/lighthouse/issues/11950
* @param {ReadonlyArray<Smokehouse.BackCompatTestDefn>} allTestDefns
* @return {Array<Smokehouse.TestDfn>}
*/
export function updateTestDefnFormat(allTestDefns: ReadonlyArray<Smokehouse.BackCompatTestDefn>): Array<Smokehouse.TestDfn>;
//# sourceMappingURL=back-compat-util.d.ts.map

View File

@@ -0,0 +1,41 @@
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* Licensed 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.
*/
/**
* COMPAT: update from the old TestDefn format (array of `expectations` per
* definition) to the new format (single `expectations` per def), doing our best
* generating some unique IDs.
* TODO: remove in Lighthouse 9+ once PubAds (and others?) are updated.
* @see https://github.com/GoogleChrome/lighthouse/issues/11950
* @param {ReadonlyArray<Smokehouse.BackCompatTestDefn>} allTestDefns
* @return {Array<Smokehouse.TestDfn>}
*/
function updateTestDefnFormat(allTestDefns) {
const expandedTestDefns = allTestDefns.map(testDefn => {
if (Array.isArray(testDefn.expectations)) {
// Create a testDefn per expectation.
return testDefn.expectations.map((expectations, index) => {
return {
...testDefn,
id: `${testDefn.id}-${index}`,
expectations,
};
});
} else {
// New object to make tsc happy.
return {
...testDefn,
expectations: testDefn.expectations,
};
}
});
return expandedTestDefns.flat();
}
export {
updateTestDefnFormat,
};

View File

@@ -0,0 +1,8 @@
/**
* @param {Smokehouse.SmokehouseLibOptions} options
*/
export function smokehouse(options: Smokehouse.SmokehouseLibOptions): Promise<{
success: boolean;
testResults: import("../smokehouse.js").SmokehouseResult[];
}>;
//# sourceMappingURL=lib.d.ts.map

View File

@@ -0,0 +1,48 @@
/**
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
* Licensed 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.
*/
/**
* @fileoverview Smoke test runner.
* Used to test integrations that run Lighthouse within a browser (i.e. LR, DevTools)
* Supports skipping and modifiying expectations to match the environment.
*/
/* eslint-disable no-console */
import cloneDeep from 'lodash/cloneDeep.js';
import smokeTests from '../core-tests.js';
import {runSmokehouse, getShardedDefinitions} from '../smokehouse.js';
/**
* @param {Smokehouse.SmokehouseLibOptions} options
*/
async function smokehouse(options) {
const {urlFilterRegex, skip, modify, shardArg, ...smokehouseOptions} = options;
const clonedTests = cloneDeep(smokeTests);
const modifiedTests = [];
for (const test of clonedTests) {
if (urlFilterRegex && !test.expectations.lhr.requestedUrl.match(urlFilterRegex)) {
continue;
}
const reasonToSkip = skip && skip(test, test.expectations);
if (reasonToSkip) {
console.log(`skipping ${test.expectations.lhr.requestedUrl}: ${reasonToSkip}`);
continue;
}
modify && modify(test, test.expectations);
modifiedTests.push(test);
}
const shardedTests = getShardedDefinitions(modifiedTests, shardArg);
return runSmokehouse(shardedTests, smokehouseOptions);
}
export {smokehouse};

View File

@@ -0,0 +1,2 @@
export * from "../smokehouse.js";
//# sourceMappingURL=node.d.ts.map

View File

@@ -0,0 +1,12 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed 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.
*/
/**
* @fileoverview A smokehouse frontend for running within a node process.
*/
// Smokehouse is runnable from within node, so just a no-op for now.
export * from '../smokehouse.js';

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
export {};
//# sourceMappingURL=smokehouse-bin.d.ts.map

View File

@@ -0,0 +1,264 @@
#!/usr/bin/env node
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed 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.
*/
/**
* @fileoverview A smokehouse frontend for running from the command line. Parse
* flags, start fixture webservers, then run smokehouse.
*/
/* eslint-disable no-console */
import path from 'path';
import fs from 'fs';
import url from 'url';
import cloneDeep from 'lodash/cloneDeep.js';
import yargs from 'yargs';
import * as yargsHelpers from 'yargs/helpers';
import log from 'lighthouse-logger';
import {runSmokehouse, getShardedDefinitions} from '../smokehouse.js';
import {updateTestDefnFormat} from './back-compat-util.js';
import {LH_ROOT} from '../../../../root.js';
import exclusions from '../config/exclusions.js';
import {saveArtifacts} from '../../../../core/lib/asset-saver.js';
import {saveLhr} from '../../../../core/lib/asset-saver.js';
const coreTestDefnsPath =
path.join(LH_ROOT, 'cli/test/smokehouse/core-tests.js');
/**
* Possible Lighthouse runners. Loaded dynamically so e.g. a CLI run isn't
* contingent on having built all the bundles.
*/
const runnerPaths = {
cli: '../lighthouse-runners/cli.js',
bundle: '../lighthouse-runners/bundle.js',
devtools: '../lighthouse-runners/devtools.js',
};
/**
* Determine batches of smoketests to run, based on the `requestedIds`.
* @param {Array<Smokehouse.TestDfn>} allTestDefns
* @param {Array<string>} requestedIds
* @param {Set<string>} excludedTests
* @return {Array<Smokehouse.TestDfn>}
*/
function getDefinitionsToRun(allTestDefns, requestedIds, excludedTests) {
let smokes = [];
const usage = ` ${log.dim}yarn smoke ${allTestDefns.map(t => t.id).join(' ')}${log.reset}\n`;
if (requestedIds.length === 0) {
smokes = [...allTestDefns];
console.log('Running ALL smoketests. Equivalent to:');
console.log(usage);
} else {
smokes = allTestDefns.filter(test => {
// Include all tests that *include* requested id.
// e.g. a requested 'pwa' will match 'pwa-airhorner', 'pwa-caltrain', etc
return requestedIds.some(requestedId => test.id.includes(requestedId));
});
console.log(`Running ONLY smoketests for: ${smokes.map(t => t.id).join(' ')}\n`);
}
const unmatchedIds = requestedIds.filter(requestedId => {
return !allTestDefns.map(t => t.id).some(id => id.includes(requestedId));
});
if (unmatchedIds.length) {
console.log(log.redify(`Smoketests not found for: ${unmatchedIds.join(' ')}`));
console.log(`Check test exclusions (${[...excludedTests].join(' ')})\n`);
console.log(usage);
}
if (!smokes.length) {
throw new Error('no smoketest found to run');
}
return smokes;
}
/**
* Prune the `networkRequests` from the test expectations when `takeNetworkRequestUrls`
* is not defined. Custom servers may not have this method available in-process.
* Also asserts that any expectation with `networkRequests` is run serially. For core
* tests, we don't currently have a good way to map requests to test definitions if
* the tests are run in parallel.
* @param {Array<Smokehouse.TestDfn>} testDefns
* @param {Function|undefined} takeNetworkRequestUrls
* @return {Array<Smokehouse.TestDfn>}
*/
function pruneExpectedNetworkRequests(testDefns, takeNetworkRequestUrls) {
const pruneNetworkRequests = !takeNetworkRequestUrls;
const clonedDefns = cloneDeep(testDefns);
for (const {id, expectations, runSerially} of clonedDefns) {
if (!runSerially && expectations.networkRequests) {
throw new Error(`'${id}' must be set to 'runSerially: true' to assert 'networkRequests'`);
}
if (pruneNetworkRequests && expectations.networkRequests) {
// eslint-disable-next-line max-len
const msg = `'networkRequests' cannot be asserted in test '${id}'. They should only be asserted on tests from an in-process server`;
if (process.env.CI) {
// If we're in CI, we require any networkRequests expectations to be asserted.
throw new Error(msg);
}
console.warn(log.redify('Warning:'),
`${msg}. Pruning expectation: ${JSON.stringify(expectations.networkRequests)}`);
expectations.networkRequests = undefined;
}
}
return clonedDefns;
}
/**
* CLI entry point.
*/
async function begin() {
const y = yargs(yargsHelpers.hideBin(process.argv));
const rawArgv = y
.help('help')
.usage('node $0 [<options>] <test-ids>')
.example('node $0 -j=1 pwa seo', 'run pwa and seo tests serially')
.option('_', {
array: true,
type: 'string',
})
.options({
'debug': {
type: 'boolean',
default: false,
describe: 'Save test artifacts and output verbose logs',
},
'legacy-navigation': {
type: 'boolean',
default: false,
describe: 'Use the legacy navigation runner',
},
'jobs': {
type: 'number',
alias: 'j',
describe: 'Manually set the number of jobs to run at once. `1` runs all tests serially',
},
'retries': {
type: 'number',
describe: 'The number of times to retry failing tests before accepting. Defaults to 0',
},
'runner': {
default: 'cli',
choices: ['cli', 'bundle', 'devtools'],
describe: 'The method of running Lighthouse',
},
'tests-path': {
type: 'string',
describe: 'The path to a set of test definitions to run. Defaults to core smoke tests.',
},
'shard': {
type: 'string',
// eslint-disable-next-line max-len
describe: 'A argument of the form "n/d", which divides the selected tests into d groups and runs the nth group. n and d must be positive integers with 1 ≤ n ≤ d.',
},
'ignore-exclusions': {
type: 'boolean',
default: false,
describe: 'Ignore any smoke test exclusions set.',
},
})
.wrap(y.terminalWidth())
.argv;
// Augmenting yargs type with auto-camelCasing breaks in tsc@4.1.2 and @types/yargs@15.0.11,
// so for now cast to add yarg's camelCase properties to type.
const argv =
/** @type {Awaited<typeof rawArgv> & LH.Util.CamelCasify<Awaited<typeof rawArgv>>} */ (rawArgv);
const jobs = Number.isFinite(argv.jobs) ? argv.jobs : undefined;
const retries = Number.isFinite(argv.retries) ? argv.retries : undefined;
const runnerPath = runnerPaths[/** @type {keyof typeof runnerPaths} */ (argv.runner)];
if (argv.runner === 'bundle') {
console.log('\n✨ Be sure to have recently run this: yarn build-all');
}
const {runLighthouse, setup} = await import(runnerPath);
runLighthouse.runnerName = argv.runner;
// Find test definition file and filter by requestedTestIds.
let testDefnPath = argv.testsPath || coreTestDefnsPath;
testDefnPath = path.resolve(process.cwd(), testDefnPath);
const requestedTestIds = argv._;
const {default: rawTestDefns} = await import(url.pathToFileURL(testDefnPath).href);
const allTestDefns = updateTestDefnFormat(rawTestDefns);
const excludedTests = new Set(exclusions[argv.runner] || []);
const filteredTestDefns = argv.ignoreExclusions ?
allTestDefns : allTestDefns.filter(test => !excludedTests.has(test.id));
const requestedTestDefns = getDefinitionsToRun(filteredTestDefns,
requestedTestIds, excludedTests);
const testDefns = getShardedDefinitions(requestedTestDefns, argv.shard);
let smokehouseResult;
let servers;
let takeNetworkRequestUrls = undefined;
try {
// If running the core tests, spin up the test server.
if (testDefnPath === coreTestDefnsPath) {
const {createServers} = await import('../../fixtures/static-server.js');
servers = await createServers();
takeNetworkRequestUrls = servers[0].takeRequestUrls.bind(servers[0]);
}
const prunedTestDefns = pruneExpectedNetworkRequests(testDefns, takeNetworkRequestUrls);
const options = {
jobs,
retries,
isDebug: argv.debug,
useLegacyNavigation: argv.legacyNavigation,
lighthouseRunner: runLighthouse,
takeNetworkRequestUrls,
setup,
};
smokehouseResult = (await runSmokehouse(prunedTestDefns, options));
} finally {
servers?.forEach(s => s.close());
}
if (!smokehouseResult.success) {
const failedTestResults = smokehouseResult.testResults.filter(r => r.failed);
// Save failed runs to directory. In CI, this is uploaded as an artifact.
const failuresDir = `${LH_ROOT}/.tmp/smokehouse-failures`;
fs.rmSync(failuresDir, {recursive: true, force: true});
fs.mkdirSync(failuresDir);
for (const testResult of failedTestResults) {
for (let i = 0; i < testResult.runs.length; i++) {
const runDir = `${failuresDir}/${i}/${testResult.id}`;
fs.mkdirSync(runDir, {recursive: true});
const run = testResult.runs[i];
await saveArtifacts(run.artifacts, runDir);
await saveLhr(run.lhr, runDir);
fs.writeFileSync(`${runDir}/assertionLog.txt`, run.assertionLog);
fs.writeFileSync(`${runDir}/lighthouseLog.txt`, run.lighthouseLog);
if (run.networkRequests) {
fs.writeFileSync(`${runDir}/networkRequests.txt`, run.networkRequests.join('\n'));
}
}
}
const cmd = `yarn smoke ${failedTestResults.map(r => r.id).join(' ')}`;
console.log(`rerun failures: ${cmd}`);
}
const exitCode = smokehouseResult.success ? 0 : 1;
process.exit(exitCode);
}
await begin();