From 5e1d2e5345030d3810c50f9fb2bc21e8349c0b11 Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 5 Sep 2025 14:15:32 +0800 Subject: [PATCH] chore: strip data-testid on prod --- cypress/e2e/page/more-page-action.cy.ts | 110 ++++++------------------ vite-plugin-strip-testid.ts | 82 ++++++++++++++++++ vite.config.ts | 3 + 3 files changed, 111 insertions(+), 84 deletions(-) create mode 100644 vite-plugin-strip-testid.ts diff --git a/cypress/e2e/page/more-page-action.cy.ts b/cypress/e2e/page/more-page-action.cy.ts index 24360b2d..74c2bbc3 100644 --- a/cypress/e2e/page/more-page-action.cy.ts +++ b/cypress/e2e/page/more-page-action.cy.ts @@ -1,7 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; -import { PageSelectors, ViewActionSelectors, SpaceSelectors, waitForReactUpdate } from '../../support/selectors'; +import { PageSelectors, waitForReactUpdate } from '../../support/selectors'; describe('More Page Actions', () => { const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL'); @@ -27,52 +27,52 @@ describe('More Page Actions', () => { // Sign in first cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - + const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(testEmail); - + cy.url().should('include', '/app'); TestTool.waitForPageLoad(3000); - + // Wait for the sidebar to load properly TestTool.waitForSidebarReady(); cy.wait(2000); - + // Skip expanding space since Getting started is already visible cy.task('log', 'Page already visible, skipping expand'); // Open the first available page from the sidebar, then trigger inline ViewActionsPopover via "..." on the row // Find the Getting started page and hover to reveal the more actions cy.task('log', 'Looking for Getting started page'); - + // Find the page by its text content cy.contains('Getting started') .parent() .parent() .trigger('mouseenter', { force: true }) .trigger('mouseover', { force: true }); - + cy.wait(1000); - + // Look for the more actions button - using PageSelectors PageSelectors.moreActionsButton().first().click({ force: true }); - + cy.task('log', 'Clicked more actions button'); // Verify core items in ViewActionsPopover // The menu should be open now, verify at least one of the common actions exists cy.get('[data-slot="dropdown-menu-content"]', { timeout: 5000 }).should('exist'); - + // Check for common menu items - they might have different test ids or text cy.get('[data-slot="dropdown-menu-content"]').within(() => { // Look for items by text content since test ids might vary cy.contains('Delete').should('exist'); - cy.contains('Duplicate').should('exist'); + cy.contains('Duplicate').should('exist'); cy.contains('Move to').should('exist'); }); }); - it('should trigger Duplicate action from More actions menu', () => { + it('should trigger Duplicate action from More actions menu', () => { // Handle uncaught exceptions during workspace creation cy.on('uncaught:exception', (err: Error) => { if (err.message.includes('No workspace or service found')) { @@ -84,114 +84,56 @@ describe('More Page Actions', () => { // Sign in first cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - + const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(testEmail); - + cy.url().should('include', '/app'); TestTool.waitForPageLoad(3000); - + // Wait for the sidebar to load properly TestTool.waitForSidebarReady(); cy.wait(2000); - + // Find the Getting started page and open its more actions menu const originalPageName = 'Getting started'; cy.task('log', `Opening More Actions for page: ${originalPageName}`); - + // Find the page by its text content and hover cy.contains(originalPageName) .parent() .parent() .trigger('mouseenter', { force: true }) .trigger('mouseover', { force: true }); - + cy.wait(1000); - + // Look for the more actions button - using PageSelectors PageSelectors.moreActionsButton().first().click({ force: true }); - + cy.task('log', 'Clicked more actions button'); - + // Click on Duplicate option which is available in the dropdown cy.get('[data-slot="dropdown-menu-content"]').within(() => { cy.contains('Duplicate').click(); }); cy.task('log', 'Clicked Duplicate option'); - + // Wait for the duplication to complete waitForReactUpdate(2000); - + // Verify the page was duplicated - there should now be two pages with similar names // The duplicated page usually has "(copy)" or similar suffix cy.contains('Getting started').should('exist'); - + // Check if there's a duplicated page (might have a suffix like "(1)" or "(copy)") PageSelectors.names().then(($pages: JQuery) => { - const pageCount = $pages.filter((index: number, el: HTMLElement) => + const pageCount = $pages.filter((index: number, el: HTMLElement) => el.textContent?.includes('Getting started')).length; expect(pageCount).to.be.at.least(1); cy.task('log', `Found ${pageCount} pages with 'Getting started' in the name`); }); - + cy.task('log', 'Page successfully duplicated'); }); - - // it('should open Change Icon popover from More actions', () => { - - // }); - - // it('should remove icon via Change Icon popover', () => { - // TestTool.morePageActionsChangeIcon(); - - // cy.get('[role="dialog"]').within(() => { - // cy.contains('button', 'Remove').click(); - // }); - - // TestTool.getModal().should('not.exist'); - // cy.get('.view-icon').should('not.exist'); - // }); - - // it('should upload a custom icon via Change Icon popover', () => { - // TestTool.morePageActionsChangeIcon(); - - // cy.get('[role="dialog"]').within(() => { - // cy.get('input[type="file"]').attachFile('test-icon.png'); - // }); - - // TestTool.getModal().should('not.exist'); - // cy.get('.view-icon').should('exist'); - // }); - - // it('should open page in a new tab from More actions', () => { - // cy.window().then((win) => { - // cy.stub(win, 'open').as('open'); - // }); - - // TestTool.morePageActionsOpenNewTab(); - // cy.get('@open').should('be.called'); - // }); - - // it('should duplicate page from More actions and show success toast', () => { - // TestTool.morePageActionsDuplicate(); - // cy.wait(2000); - // TestTool.getPageByName('Get started').should('have.length', 2); - // }); - - // it('should move page to another space from More actions', () => { - // TestTool.morePageActionsMoveTo(); - - // cy.get('[role="dialog"]').within(() => { - // TestTool.getSpaceItems().first().click(); - // }); - // cy.wait(2000); - // TestTool.getPageByName('Get started').should('be.visible'); - // }); - - // it('should delete page from More actions and confirm deletion', () => { - // TestTool.morePageActionsDelete(); - // TestTool.confirmPageDeletion(); - // cy.wait(2000); - // TestTool.getPageByName('Get started').should('not.exist'); - // }); }); \ No newline at end of file diff --git a/vite-plugin-strip-testid.ts b/vite-plugin-strip-testid.ts new file mode 100644 index 00000000..7f255055 --- /dev/null +++ b/vite-plugin-strip-testid.ts @@ -0,0 +1,82 @@ +import { Plugin } from 'vite'; + +/** + * Vite plugin to strip data-testid attributes from production builds + * This reduces bundle size and removes test-specific attributes from production code + */ +export function stripTestIdPlugin(): Plugin { + return { + name: 'strip-test-id', + apply: 'build', // Only apply during build, not dev + transform(code: string, id: string) { + // Skip node_modules to avoid transforming external libraries + if (id.includes('node_modules')) { + return null; + } + + // Only process .tsx and .jsx files from our source + if (!id.match(/src.*\.(tsx|jsx)$/)) { + return null; + } + + // Only strip in production builds + if (process.env.NODE_ENV !== 'production') { + return null; + } + + let transformedCode = code; + let hasChanges = false; + + try { + // Pattern 1: Simple string attributes: data-testid="value" or data-testid='value' + // This is the safest pattern to remove + const simpleStringPattern = /\s+data-testid\s*=\s*["'][^"']*["']/g; + const matches = transformedCode.match(simpleStringPattern); + if (matches && matches.length > 0) { + console.log(`Stripping ${matches.length} data-testid attributes from ${id}`); + transformedCode = transformedCode.replace(simpleStringPattern, ''); + hasChanges = true; + } + + // Pattern 2: Simple expressions without nested braces: data-testid={variable} + const simpleExpressionPattern = /\s+data-testid\s*=\s*\{[^{}]+\}/g; + const exprMatches = transformedCode.match(simpleExpressionPattern); + if (exprMatches && exprMatches.length > 0) { + console.log(`Stripping ${exprMatches.length} data-testid expressions from ${id}`); + transformedCode = transformedCode.replace(simpleExpressionPattern, ''); + hasChanges = true; + } + + // Pattern 3: Template literals: data-testid={`value-${id}`} + const templatePattern = /\s+data-testid\s*=\s*\{`[^`]*`\}/g; + if (templatePattern.test(transformedCode)) { + transformedCode = transformedCode.replace(templatePattern, ''); + hasChanges = true; + } + + if (hasChanges) { + // Quick validation: check for obvious syntax errors + // Count opening and closing braces to ensure we didn't break anything + const openBraces = (transformedCode.match(/\{/g) || []).length; + const closeBraces = (transformedCode.match(/\}/g) || []).length; + + if (openBraces !== closeBraces) { + console.warn(`Warning: Brace mismatch after transformation in ${id}. Skipping transformation.`); + return null; + } + + return { + code: transformedCode, + map: null, + }; + } + } catch (error) { + console.error(`Error processing ${id}:`, error); + // Return null to skip transformation on error + return null; + } + + return null; + }, + }; +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 1ebefd69..8ec335f8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,6 +7,7 @@ import { createHtmlPlugin } from 'vite-plugin-html'; import istanbul from 'vite-plugin-istanbul'; import svgr from 'vite-plugin-svgr'; import { totalBundleSize } from 'vite-plugin-total-bundle-size'; +import { stripTestIdPlugin } from './vite-plugin-strip-testid'; const resourcesPath = path.resolve(__dirname, '../resources'); const isDev = process.env.NODE_ENV === 'development'; @@ -17,6 +18,8 @@ const isTest = process.env.NODE_ENV === 'test' || process.env.COVERAGE === 'true export default defineConfig({ plugins: [ react(), + // Strip data-testid attributes in production builds + isProd ? stripTestIdPlugin() : undefined, createHtmlPlugin({ inject: { data: {