feat: lazy load ninja-keys + HTML Invoker Commands API
- Lazy load ninja-keys only on CMD+K press (0 requests on initial load) - Use esm.sh bundled module (3 requests vs ~81 previously) - Add esm.sh to CSP whitelist - Implement HTML Invoker Commands API for modals: - commandfor="modal-id" + command="show-modal" for opening - commandfor="modal-id" + command="close" for closing - Removes need for onclick handlers on modal buttons - Refactor index.html into layout partials (head, body-scripts) - Add comprehensive tests for both features
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* HTML INVOKER COMMANDS API TEST
|
||||
* ==============================
|
||||
* Tests the new HTML commandfor/command attributes for modals:
|
||||
* - Buttons have commandfor and command attributes
|
||||
* - command="show-modal" opens dialogs
|
||||
* - command="close" closes dialogs
|
||||
* - No onclick handlers for modal operations
|
||||
*
|
||||
* Browser support: Chrome/Edge 135+, Firefox Nightly, Safari TP
|
||||
* @see https://developer.chrome.com/blog/command-and-commandfor
|
||||
*/
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const URL = "http://localhost:1999";
|
||||
|
||||
async function testInvokerCommands() {
|
||||
console.log('🎯 HTML INVOKER COMMANDS API TEST\n');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
const browser = await chromium.launch({ headless: process.env.HEADLESS === 'true' });
|
||||
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
|
||||
|
||||
const testResults = [];
|
||||
|
||||
await page.goto(URL);
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// ========================================================================
|
||||
// TEST 1: Buttons have commandfor and command attributes
|
||||
// ========================================================================
|
||||
console.log('\n1️⃣ Testing buttons have commandfor/command attributes...');
|
||||
|
||||
const buttonsWithCommand = await page.evaluate(() => {
|
||||
const buttons = document.querySelectorAll('[commandfor]');
|
||||
return Array.from(buttons).map(btn => ({
|
||||
id: btn.id || btn.className.split(' ')[0],
|
||||
commandfor: btn.getAttribute('commandfor'),
|
||||
command: btn.getAttribute('command'),
|
||||
hasOnclick: btn.hasAttribute('onclick')
|
||||
}));
|
||||
});
|
||||
|
||||
console.log(` Found ${buttonsWithCommand.length} buttons with commandfor attribute:`);
|
||||
buttonsWithCommand.forEach(btn => {
|
||||
console.log(` - ${btn.id}: commandfor="${btn.commandfor}" command="${btn.command}" onclick=${btn.hasOnclick}`);
|
||||
});
|
||||
|
||||
const hasCommandButtons = buttonsWithCommand.length >= 6; // At least 6 modal buttons
|
||||
console.log(` ${hasCommandButtons ? '✅ PASS' : '❌ FAIL'} - At least 6 buttons use commandfor`);
|
||||
testResults.push({ test: 'Buttons have commandfor attributes', passed: hasCommandButtons });
|
||||
|
||||
// ========================================================================
|
||||
// TEST 2: No onclick for showModal/close
|
||||
// ========================================================================
|
||||
console.log('\n2️⃣ Testing no onclick handlers for modal operations...');
|
||||
|
||||
const noOnclickForModals = buttonsWithCommand.every(btn => !btn.hasOnclick);
|
||||
console.log(` All command buttons without onclick: ${noOnclickForModals}`);
|
||||
console.log(` ${noOnclickForModals ? '✅ PASS' : '❌ FAIL'} - No onclick handlers on command buttons`);
|
||||
testResults.push({ test: 'No onclick on command buttons', passed: noOnclickForModals });
|
||||
|
||||
// ========================================================================
|
||||
// TEST 3: Info button opens info-modal
|
||||
// ========================================================================
|
||||
console.log('\n3️⃣ Testing info button opens modal via command attribute...');
|
||||
|
||||
const infoButton = await page.$('#info-button');
|
||||
if (infoButton) {
|
||||
const infoAttrs = await page.$eval('#info-button', el => ({
|
||||
commandfor: el.getAttribute('commandfor'),
|
||||
command: el.getAttribute('command')
|
||||
}));
|
||||
console.log(` Info button: commandfor="${infoAttrs.commandfor}" command="${infoAttrs.command}"`);
|
||||
|
||||
// Click the button
|
||||
await infoButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const infoModalOpen = await page.evaluate(() => {
|
||||
const modal = document.getElementById('info-modal');
|
||||
return modal && modal.hasAttribute('open');
|
||||
});
|
||||
|
||||
console.log(` Info modal opened: ${infoModalOpen}`);
|
||||
console.log(` ${infoModalOpen ? '✅ PASS' : '❌ FAIL'} - command="show-modal" works`);
|
||||
testResults.push({ test: 'command="show-modal" opens dialog', passed: infoModalOpen });
|
||||
|
||||
// Test close button
|
||||
if (infoModalOpen) {
|
||||
const closeButton = await page.$('#info-modal [command="close"]');
|
||||
if (closeButton) {
|
||||
await closeButton.click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
const modalClosed = await page.evaluate(() => {
|
||||
const modal = document.getElementById('info-modal');
|
||||
return modal && !modal.hasAttribute('open');
|
||||
});
|
||||
|
||||
console.log(` Info modal closed: ${modalClosed}`);
|
||||
console.log(` ${modalClosed ? '✅ PASS' : '❌ FAIL'} - command="close" works`);
|
||||
testResults.push({ test: 'command="close" closes dialog', passed: modalClosed });
|
||||
} else {
|
||||
console.log(' ⚠️ Close button with command="close" not found');
|
||||
await page.keyboard.press('Escape');
|
||||
testResults.push({ test: 'command="close" closes dialog', passed: false });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(' ⚠️ Info button not found');
|
||||
testResults.push({ test: 'command="show-modal" opens dialog', passed: false });
|
||||
testResults.push({ test: 'command="close" closes dialog', passed: false });
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// TEST 4: Contact button opens contact-modal
|
||||
// ========================================================================
|
||||
console.log('\n4️⃣ Testing contact button opens modal...');
|
||||
|
||||
const contactButton = await page.$('#contact-button');
|
||||
if (contactButton) {
|
||||
await contactButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const contactModalOpen = await page.evaluate(() => {
|
||||
const modal = document.getElementById('contact-modal');
|
||||
return modal && modal.hasAttribute('open');
|
||||
});
|
||||
|
||||
console.log(` Contact modal opened: ${contactModalOpen}`);
|
||||
console.log(` ${contactModalOpen ? '✅ PASS' : '❌ FAIL'} - Contact modal opens`);
|
||||
testResults.push({ test: 'Contact modal opens via command', passed: contactModalOpen });
|
||||
|
||||
// Close with ESC
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForTimeout(300);
|
||||
} else {
|
||||
console.log(' ⚠️ Contact button not found');
|
||||
testResults.push({ test: 'Contact modal opens via command', passed: false });
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// TEST 5: Shortcuts button opens shortcuts-modal
|
||||
// ========================================================================
|
||||
console.log('\n5️⃣ Testing shortcuts button opens modal...');
|
||||
|
||||
const shortcutsButton = await page.$('#shortcuts-button');
|
||||
if (shortcutsButton) {
|
||||
await shortcutsButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const shortcutsModalOpen = await page.evaluate(() => {
|
||||
const modal = document.getElementById('shortcuts-modal');
|
||||
return modal && modal.hasAttribute('open');
|
||||
});
|
||||
|
||||
console.log(` Shortcuts modal opened: ${shortcutsModalOpen}`);
|
||||
console.log(` ${shortcutsModalOpen ? '✅ PASS' : '❌ FAIL'} - Shortcuts modal opens`);
|
||||
testResults.push({ test: 'Shortcuts modal opens via command', passed: shortcutsModalOpen });
|
||||
|
||||
// Close with ESC
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForTimeout(300);
|
||||
} else {
|
||||
console.log(' ⚠️ Shortcuts button not found');
|
||||
testResults.push({ test: 'Shortcuts modal opens via command', passed: false });
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// TEST 6: PDF button in action bar opens pdf-modal
|
||||
// ========================================================================
|
||||
console.log('\n6️⃣ Testing PDF action button opens modal...');
|
||||
|
||||
const pdfActionButton = await page.$('#action-bar-pdf-btn');
|
||||
if (pdfActionButton) {
|
||||
const pdfAttrs = await page.$eval('#action-bar-pdf-btn', el => ({
|
||||
commandfor: el.getAttribute('commandfor'),
|
||||
command: el.getAttribute('command')
|
||||
}));
|
||||
console.log(` PDF button: commandfor="${pdfAttrs.commandfor}" command="${pdfAttrs.command}"`);
|
||||
|
||||
await pdfActionButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const pdfModalOpen = await page.evaluate(() => {
|
||||
const modal = document.getElementById('pdf-modal');
|
||||
return modal && modal.hasAttribute('open');
|
||||
});
|
||||
|
||||
console.log(` PDF modal opened: ${pdfModalOpen}`);
|
||||
console.log(` ${pdfModalOpen ? '✅ PASS' : '❌ FAIL'} - PDF modal opens via command`);
|
||||
testResults.push({ test: 'PDF modal opens via command', passed: pdfModalOpen });
|
||||
|
||||
// Close with ESC
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForTimeout(300);
|
||||
} else {
|
||||
console.log(' ⚠️ PDF action button not found');
|
||||
testResults.push({ test: 'PDF modal opens via command', passed: false });
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// FINAL SUMMARY
|
||||
// ========================================================================
|
||||
console.log("\n" + "=".repeat(70));
|
||||
console.log("📊 TEST SUMMARY\n");
|
||||
|
||||
const totalTests = testResults.length;
|
||||
const passedTests = testResults.filter(r => r.passed).length;
|
||||
const failedTests = totalTests - passedTests;
|
||||
|
||||
testResults.forEach(result => {
|
||||
console.log(` ${result.passed ? '✅' : '❌'} ${result.test}`);
|
||||
});
|
||||
|
||||
console.log(`\n Total: ${passedTests}/${totalTests} tests passed`);
|
||||
console.log("=".repeat(70) + "\n");
|
||||
|
||||
if (failedTests === 0) {
|
||||
console.log("🎉 ALL HTML INVOKER COMMANDS TESTS PASSED!");
|
||||
} else {
|
||||
console.log("⚠️ SOME TESTS FAILED - See details above");
|
||||
console.log(" Note: command/commandfor requires Chrome 135+, Edge 135+, Firefox Nightly, Safari TP");
|
||||
}
|
||||
|
||||
// Auto-close after tests if HEADLESS env is set
|
||||
if (process.env.HEADLESS === 'true') {
|
||||
await browser.close();
|
||||
process.exit(failedTests === 0 ? 0 : 1);
|
||||
} else {
|
||||
console.log("\nBrowser will stay open for manual inspection.");
|
||||
console.log("Press Ctrl+C when done.\n");
|
||||
await new Promise(() => {}); // Keep browser open
|
||||
}
|
||||
}
|
||||
|
||||
await testInvokerCommands();
|
||||
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* CMD+K LAZY LOADING TEST
|
||||
* =======================
|
||||
* Tests that ninja-keys is lazy-loaded only when needed:
|
||||
* - No ninja-keys element on initial page load
|
||||
* - No esm.sh/ninja-keys loaded initially
|
||||
* - CMD+K triggers dynamic import
|
||||
* - ninja-keys element created and opened
|
||||
* - Subsequent uses don't reload
|
||||
*
|
||||
* This optimization reduces initial page load by ~15 module requests
|
||||
*/
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const URL = "http://localhost:1999";
|
||||
|
||||
async function testCmdKLazyLoading() {
|
||||
console.log('🚀 CMD+K LAZY LOADING TEST\n');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
const browser = await chromium.launch({ headless: process.env.HEADLESS === 'true' });
|
||||
const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const testResults = [];
|
||||
const networkRequests = [];
|
||||
|
||||
// Monitor network requests
|
||||
page.on('request', request => {
|
||||
const url = request.url();
|
||||
if (url.includes('ninja-keys') || url.includes('esm.sh')) {
|
||||
networkRequests.push({
|
||||
url,
|
||||
timestamp: Date.now(),
|
||||
type: request.resourceType()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(URL);
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const initialRequestCount = networkRequests.length;
|
||||
console.log(` Initial ninja-keys/esm.sh requests: ${initialRequestCount}`);
|
||||
|
||||
// ========================================================================
|
||||
// TEST 1: No ninja-keys element on initial load
|
||||
// ========================================================================
|
||||
console.log('\n1️⃣ Testing no ninja-keys on initial load...');
|
||||
|
||||
const ninjaKeysOnLoad = await page.evaluate(() => {
|
||||
const nk = document.getElementById('cmd-k-bar');
|
||||
return {
|
||||
exists: !!nk,
|
||||
tagName: nk?.tagName || 'N/A'
|
||||
};
|
||||
});
|
||||
|
||||
console.log(` ninja-keys element exists: ${ninjaKeysOnLoad.exists}`);
|
||||
const noInitialElement = !ninjaKeysOnLoad.exists;
|
||||
console.log(` ${noInitialElement ? '✅ PASS' : '❌ FAIL'} - No ninja-keys element on initial load`);
|
||||
testResults.push({ test: 'No ninja-keys element on initial load', passed: noInitialElement });
|
||||
|
||||
// ========================================================================
|
||||
// TEST 2: No esm.sh requests on initial load
|
||||
// ========================================================================
|
||||
console.log('\n2️⃣ Testing no esm.sh requests on initial load...');
|
||||
|
||||
const noInitialRequests = initialRequestCount === 0;
|
||||
console.log(` esm.sh requests before interaction: ${initialRequestCount}`);
|
||||
console.log(` ${noInitialRequests ? '✅ PASS' : '❌ FAIL'} - No ninja-keys loaded initially`);
|
||||
testResults.push({ test: 'No esm.sh requests on initial load', passed: noInitialRequests });
|
||||
|
||||
// ========================================================================
|
||||
// TEST 3: Container element exists for lazy loading
|
||||
// ========================================================================
|
||||
console.log('\n3️⃣ Testing container element exists...');
|
||||
|
||||
const containerExists = await page.evaluate(() => {
|
||||
const container = document.getElementById('cmd-k-container');
|
||||
return !!container;
|
||||
});
|
||||
|
||||
console.log(` cmd-k-container exists: ${containerExists}`);
|
||||
console.log(` ${containerExists ? '✅ PASS' : '❌ FAIL'} - Container ready for lazy loading`);
|
||||
testResults.push({ test: 'Container element exists', passed: containerExists });
|
||||
|
||||
// ========================================================================
|
||||
// TEST 4: CMD+K triggers lazy load
|
||||
// ========================================================================
|
||||
console.log('\n4️⃣ Testing CMD+K triggers lazy load...');
|
||||
|
||||
const requestsBeforeCmdK = networkRequests.length;
|
||||
|
||||
// Press CMD+K (Mac) or Ctrl+K (Windows/Linux)
|
||||
await page.keyboard.press('Meta+k');
|
||||
await page.waitForTimeout(3000); // Wait for module to load and custom element to register
|
||||
|
||||
const requestsAfterCmdK = networkRequests.length;
|
||||
const newRequests = requestsAfterCmdK - requestsBeforeCmdK;
|
||||
|
||||
console.log(` Requests before CMD+K: ${requestsBeforeCmdK}`);
|
||||
console.log(` Requests after CMD+K: ${requestsAfterCmdK}`);
|
||||
console.log(` New esm.sh requests: ${newRequests}`);
|
||||
|
||||
const lazyLoadTriggered = newRequests > 0;
|
||||
console.log(` ${lazyLoadTriggered ? '✅ PASS' : '❌ FAIL'} - CMD+K triggers module load`);
|
||||
testResults.push({ test: 'CMD+K triggers lazy load', passed: lazyLoadTriggered });
|
||||
|
||||
// ========================================================================
|
||||
// TEST 5: ninja-keys element created after CMD+K
|
||||
// ========================================================================
|
||||
console.log('\n5️⃣ Testing ninja-keys element created...');
|
||||
|
||||
const ninjaKeysAfterCmdK = await page.evaluate(() => {
|
||||
const nk = document.getElementById('cmd-k-bar');
|
||||
if (!nk) return { exists: false, tagName: 'N/A', isOpen: false };
|
||||
// ninja-keys uses shadow DOM with .modal.visible class
|
||||
const shadow = nk.shadowRoot;
|
||||
const modal = shadow?.querySelector('.modal');
|
||||
return {
|
||||
exists: true,
|
||||
tagName: nk.tagName,
|
||||
isOpen: modal?.classList?.contains('visible') || false
|
||||
};
|
||||
});
|
||||
|
||||
console.log(` ninja-keys exists: ${ninjaKeysAfterCmdK.exists}`);
|
||||
console.log(` ninja-keys tag: ${ninjaKeysAfterCmdK.tagName}`);
|
||||
console.log(` ninja-keys open: ${ninjaKeysAfterCmdK.isOpen}`);
|
||||
|
||||
const elementCreated = ninjaKeysAfterCmdK.exists && ninjaKeysAfterCmdK.tagName === 'NINJA-KEYS';
|
||||
console.log(` ${elementCreated ? '✅ PASS' : '❌ FAIL'} - ninja-keys element created`);
|
||||
testResults.push({ test: 'ninja-keys element created', passed: elementCreated });
|
||||
|
||||
// Close ninja-keys
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// ========================================================================
|
||||
// TEST 6: Subsequent CMD+K doesn't reload module
|
||||
// ========================================================================
|
||||
console.log('\n6️⃣ Testing subsequent CMD+K doesn\'t reload...');
|
||||
|
||||
const requestsBeforeSecond = networkRequests.length;
|
||||
|
||||
// Press CMD+K again
|
||||
await page.keyboard.press('Meta+k');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const requestsAfterSecond = networkRequests.length;
|
||||
const additionalRequests = requestsAfterSecond - requestsBeforeSecond;
|
||||
|
||||
console.log(` Requests before 2nd CMD+K: ${requestsBeforeSecond}`);
|
||||
console.log(` Requests after 2nd CMD+K: ${requestsAfterSecond}`);
|
||||
console.log(` Additional requests: ${additionalRequests}`);
|
||||
|
||||
const noReload = additionalRequests === 0;
|
||||
console.log(` ${noReload ? '✅ PASS' : '❌ FAIL'} - No module reload on subsequent use`);
|
||||
testResults.push({ test: 'No module reload on subsequent use', passed: noReload });
|
||||
|
||||
// Close ninja-keys
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// ========================================================================
|
||||
// TEST 7: Button click also triggers lazy load
|
||||
// ========================================================================
|
||||
console.log('\n7️⃣ Testing button click works with lazy-loaded ninja-keys...');
|
||||
|
||||
const cmdKButton = await page.$('#cmd-k-button');
|
||||
if (cmdKButton) {
|
||||
await cmdKButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const openAfterClick = await page.evaluate(() => {
|
||||
const nk = document.getElementById('cmd-k-bar');
|
||||
if (!nk) return false;
|
||||
// ninja-keys uses shadow DOM with .modal.visible class
|
||||
const shadow = nk.shadowRoot;
|
||||
const modal = shadow?.querySelector('.modal');
|
||||
return modal?.classList?.contains('visible') || false;
|
||||
});
|
||||
|
||||
console.log(` ninja-keys opened via button: ${openAfterClick}`);
|
||||
console.log(` ${openAfterClick ? '✅ PASS' : '❌ FAIL'} - Button click opens ninja-keys`);
|
||||
testResults.push({ test: 'Button click opens ninja-keys', passed: openAfterClick });
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
} else {
|
||||
console.log(' ⚠️ CMD+K button not found');
|
||||
testResults.push({ test: 'Button click opens ninja-keys', passed: false });
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// TEST 8: esm.sh used instead of unpkg (no redirect chains)
|
||||
// ========================================================================
|
||||
console.log('\n8️⃣ Testing esm.sh CDN used (no 302 redirects)...');
|
||||
|
||||
const esmShRequests = networkRequests.filter(r => r.url.includes('esm.sh'));
|
||||
const unpkgRequests = networkRequests.filter(r => r.url.includes('unpkg.com/ninja-keys'));
|
||||
|
||||
console.log(` esm.sh requests: ${esmShRequests.length}`);
|
||||
console.log(` unpkg ninja-keys requests: ${unpkgRequests.length}`);
|
||||
|
||||
const usesEsmSh = esmShRequests.length > 0 && unpkgRequests.length === 0;
|
||||
console.log(` ${usesEsmSh ? '✅ PASS' : '❌ FAIL'} - Uses esm.sh CDN (pre-bundled)`);
|
||||
testResults.push({ test: 'Uses esm.sh CDN (no redirects)', passed: usesEsmSh });
|
||||
|
||||
// ========================================================================
|
||||
// FINAL SUMMARY
|
||||
// ========================================================================
|
||||
console.log("\n" + "=".repeat(70));
|
||||
console.log("📊 TEST SUMMARY\n");
|
||||
|
||||
const totalTests = testResults.length;
|
||||
const passedTests = testResults.filter(r => r.passed).length;
|
||||
const failedTests = totalTests - passedTests;
|
||||
|
||||
testResults.forEach(result => {
|
||||
console.log(` ${result.passed ? '✅' : '❌'} ${result.test}`);
|
||||
});
|
||||
|
||||
console.log(`\n Total: ${passedTests}/${totalTests} tests passed`);
|
||||
|
||||
// Show all network requests for debugging
|
||||
if (networkRequests.length > 0) {
|
||||
console.log('\n Network requests (ninja-keys/esm.sh):');
|
||||
networkRequests.forEach((r, i) => {
|
||||
console.log(` ${i + 1}. ${r.url.substring(0, 80)}...`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log("=".repeat(70) + "\n");
|
||||
|
||||
if (failedTests === 0) {
|
||||
console.log("🎉 ALL CMD+K LAZY LOADING TESTS PASSED!");
|
||||
console.log(" Initial page load has NO ninja-keys overhead!");
|
||||
} else {
|
||||
console.log("⚠️ SOME TESTS FAILED - See details above");
|
||||
}
|
||||
|
||||
// Auto-close after tests if HEADLESS env is set
|
||||
if (process.env.HEADLESS === 'true') {
|
||||
await browser.close();
|
||||
process.exit(failedTests === 0 ? 0 : 1);
|
||||
} else {
|
||||
console.log("\nBrowser will stay open for manual inspection.");
|
||||
console.log("Press Ctrl+C when done.\n");
|
||||
await new Promise(() => {}); // Keep browser open
|
||||
}
|
||||
}
|
||||
|
||||
await testCmdKLazyLoading();
|
||||
Reference in New Issue
Block a user