diff --git a/cypress-e2e/fixtures/constants.ts b/cypress-e2e/fixtures/constants.ts index 6130f0a74bd..fd6f2874e7b 100644 --- a/cypress-e2e/fixtures/constants.ts +++ b/cypress-e2e/fixtures/constants.ts @@ -714,7 +714,7 @@ export class Constants { consumerCoreSectionButton: 'Button imported from /core', }, otherSectionCodeBlock: - '{ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }', + '{\n "userId": 1,\n "id": 1,\n "title": "delectus aut autem",\n "completed": false\n}', consumerSection: { header: 'This is /consumer.', importMessages: { diff --git a/playwright-e2e/common/base.ts b/playwright-e2e/common/base.ts index b69f72a26a7..201c27a6255 100644 --- a/playwright-e2e/common/base.ts +++ b/playwright-e2e/common/base.ts @@ -1,4 +1,4 @@ -import { expect, Locator, Page } from '@playwright/test'; +import { expect, Locator, Page, test } from '@playwright/test'; import type { ElementHandle } from 'playwright'; interface VisibilityOptions { @@ -25,6 +25,29 @@ interface ClickWithTextOptions { parentSelector?: string; isTargetChanged?: boolean; index?: number; + wait?: number; +} + +interface BrowserAlertOptions { + selector: string; + alertMessage?: string; + isEqual?: boolean; + index?: number; + parentSelector?: string; + wait?: number; +} + +interface BrowserAlertForMultipleHostsOptions extends BrowserAlertOptions { + host: number; +} + +interface CompareHostsOptions { + selector: string; + extraHost: number; + isEqual?: boolean; + index?: number; + clickSelector?: string; + wait?: number; } interface ElementContainTextOptions { @@ -68,8 +91,16 @@ export class BaseMethods { constructor(protected readonly page: Page) {} private resolveLocator(selector: string, options: { parentSelector?: string; text?: string; index?: number } = {}): Locator { + return this.resolveLocatorForPage(this.page, selector, options); + } + + private resolveLocatorForPage( + page: Page, + selector: string, + options: { parentSelector?: string; text?: string; index?: number } = {}, + ): Locator { const { parentSelector, text, index } = options; - let locator = parentSelector ? this.page.locator(parentSelector).locator(selector) : this.page.locator(selector); + let locator = parentSelector ? page.locator(parentSelector).locator(selector) : page.locator(selector); if (text) { locator = locator.filter({ hasText: text }); @@ -200,7 +231,7 @@ export class BaseMethods { await expect(locator).not.toHaveCount(0); } - async clickElementWithText({ selector, text, parentSelector, isTargetChanged = false, index }: ClickWithTextOptions): Promise { + async clickElementWithText({ selector, text, parentSelector, isTargetChanged = false, index, wait }: ClickWithTextOptions): Promise { const locator = this.resolveLocator(selector, { parentSelector, text, index }); const element = locator.first(); @@ -209,6 +240,10 @@ export class BaseMethods { } await element.click(); + + if (wait && wait > 0) { + await this.page.waitForTimeout(wait); + } } async checkElementContainText({ selector, text, isContain = true, index, parentSelector }: ElementContainTextOptions): Promise { @@ -364,16 +399,149 @@ export class BaseMethods { await poller.not.toContain(urlPart); } + skipTestByCondition(condition: unknown, reason: string = 'Skipped by condition'): void { + if (condition) { + test.info().skip(reason); + } + } + + async checkBrowserAlertByText({ + selector, + alertMessage, + isEqual = true, + index = 0, + parentSelector, + wait = 0, + }: BrowserAlertOptions): Promise { + const locator = this.resolveLocator(selector, { parentSelector, index }); + + if (wait > 0) { + await this.page.waitForTimeout(wait); + } + + const message = await this.captureDialogMessage(this.page, locator.first()); + + if (alertMessage !== undefined) { + if (isEqual) { + expect(message).toBe(alertMessage); + } else { + expect(message).not.toBe(alertMessage); + } + } + } + + async checkBrowserAlertForMultipleHosts({ + selector, + alertMessage, + isEqual = true, + index = 0, + parentSelector, + host, + wait = 0, + }: BrowserAlertForMultipleHostsOptions): Promise { + const baseGroup = this.resolveLocator(selector, { parentSelector }); + const baseCount = await baseGroup.count(); + + if (baseCount === 0) { + throw new Error(`No elements found for selector "${selector}" on the base page.`); + } + + const targetIndex = Math.min(index, baseCount - 1); + + if (wait > 0) { + await this.page.waitForTimeout(wait); + } + + const baseMessage = await this.captureDialogMessage(this.page, baseGroup.nth(targetIndex)); + + if (alertMessage !== undefined) { + if (isEqual) { + expect(baseMessage).toBe(alertMessage); + } else { + expect(baseMessage).not.toBe(alertMessage); + } + } + + const remotePage = await this.page.context().newPage(); + + try { + await remotePage.goto(`http://localhost:${host}/`, { waitUntil: 'networkidle' }); + + const remoteGroup = this.resolveLocatorForPage(remotePage, selector, { parentSelector }); + const remoteCount = await remoteGroup.count(); + + if (remoteCount === 0) { + throw new Error(`No elements found for selector "${selector}" on host ${host}.`); + } + + const remoteIndex = Math.min(targetIndex, remoteCount - 1); + const remoteMessage = await this.captureDialogMessage(remotePage, remoteGroup.nth(remoteIndex)); + + if (wait > 0) { + await remotePage.waitForTimeout(wait); + } + + if (isEqual) { + if (alertMessage !== undefined) { + expect(remoteMessage).toBe(alertMessage); + } + + expect(remoteMessage).toBe(baseMessage); + } else { + if (alertMessage !== undefined) { + expect(remoteMessage).not.toBe(alertMessage); + } + + expect(remoteMessage).not.toBe(baseMessage); + } + } finally { + await remotePage.close(); + } + } + async compareInfoBetweenHosts( - selector: string, - extraHost: number, - isEqual: boolean = true, - index: number = 0, - clickSelector?: string, - wait: number = 0, + selectorOrOptions: string | CompareHostsOptions, + extraHostArg?: number, + isEqualArg: boolean = true, + indexArg: number = 0, + clickSelectorArg?: string, + waitArg: number = 0, ): Promise { - const baseLocator = this.page.locator(selector).nth(index); - const baseText = (await baseLocator.innerText()).trim(); + let selector: string; + let extraHost: number; + let isEqual: boolean; + let index: number; + let clickSelector: string | undefined; + let wait: number; + + if (typeof selectorOrOptions === 'string') { + selector = selectorOrOptions; + if (typeof extraHostArg !== 'number') { + throw new Error('The "extraHost" parameter must be provided when using the positional signature.'); + } + extraHost = extraHostArg; + isEqual = isEqualArg; + index = indexArg; + clickSelector = clickSelectorArg; + wait = waitArg; + } else { + ({ selector, extraHost, isEqual = true, index = 0, clickSelector, wait = 0 } = selectorOrOptions); + } + + const baseGroup = this.page.locator(selector); + const baseCount = await baseGroup.count(); + + if (baseCount === 0) { + throw new Error(`No elements found for selector "${selector}" on the base page.`); + } + + const targetIndex = Math.min(index, baseCount - 1); + + if (wait > 0) { + await this.page.waitForTimeout(wait); + } + + const baseText = (await baseGroup.nth(targetIndex).innerText()).trim(); const remotePage = await this.page.context().newPage(); @@ -381,13 +549,40 @@ export class BaseMethods { await remotePage.goto(`http://localhost:${extraHost}/`, { waitUntil: 'networkidle' }); if (clickSelector) { - await remotePage.locator(clickSelector).click(); + const remoteClickGroup = remotePage.locator(clickSelector); + const remoteClickCount = await remoteClickGroup.count(); + + if (remoteClickCount === 0) { + throw new Error(`No elements found for selector "${clickSelector}" on host ${extraHost}.`); + } + + const clickIndex = Math.min(targetIndex, remoteClickCount - 1); + + try { + await this.captureDialogMessage(remotePage, remoteClickGroup.nth(clickIndex)); + } catch (error) { + const isTimeoutError = error instanceof Error && /Timeout/.test(error.message); + if (isTimeoutError) { + await remoteClickGroup.nth(clickIndex).click(); + } else { + throw error; + } + } + if (wait > 0) { await remotePage.waitForTimeout(wait); } } - const remoteText = (await remotePage.locator(selector).nth(index).innerText()).trim(); + const remoteGroup = remotePage.locator(selector); + const remoteCount = await remoteGroup.count(); + + if (remoteCount === 0) { + throw new Error(`No elements found for selector "${selector}" on host ${extraHost}.`); + } + + const remoteIndex = Math.min(targetIndex, remoteCount - 1); + const remoteText = (await remoteGroup.nth(remoteIndex).innerText()).trim(); if (isEqual) { expect(remoteText).toBe(baseText); @@ -414,4 +609,16 @@ export class BaseMethods { return style.getPropertyValue(property as string); }, prop); } + + private async captureDialogMessage(page: Page, locator: Locator): Promise { + const [dialog] = await Promise.all([ + page.waitForEvent('dialog', { timeout: 5_000 }), + locator.click(), + ]); + + const message = dialog.message(); + await dialog.accept(); + + return message; + } } diff --git a/playwright-e2e/fixtures/constants.ts b/playwright-e2e/fixtures/constants.ts index 6130f0a74bd..fd6f2874e7b 100644 --- a/playwright-e2e/fixtures/constants.ts +++ b/playwright-e2e/fixtures/constants.ts @@ -714,7 +714,7 @@ export class Constants { consumerCoreSectionButton: 'Button imported from /core', }, otherSectionCodeBlock: - '{ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }', + '{\n "userId": 1,\n "id": 1,\n "title": "delectus aut autem",\n "completed": false\n}', consumerSection: { header: 'This is /consumer.', importMessages: { diff --git a/vue-cli/consumer/vue.config.js b/vue-cli/consumer/vue.config.js index 892cc8699df..e6de3c7a762 100644 --- a/vue-cli/consumer/vue.config.js +++ b/vue-cli/consumer/vue.config.js @@ -1,6 +1,7 @@ const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin; module.exports = { + lintOnSave: false, publicPath: 'http://localhost:8080/', configureWebpack: { optimization: { diff --git a/vue-cli/core/vue.config.js b/vue-cli/core/vue.config.js index c5c3f4a9aa4..fc13ec5dfa1 100644 --- a/vue-cli/core/vue.config.js +++ b/vue-cli/core/vue.config.js @@ -1,6 +1,7 @@ const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin; module.exports = { + lintOnSave: false, publicPath: 'http://localhost:9000/', configureWebpack: { optimization: { diff --git a/vue-cli/e2e/methods/methods.ts b/vue-cli/e2e/methods/methods.ts index 2cfd835d44b..316bdb7c17d 100644 --- a/vue-cli/e2e/methods/methods.ts +++ b/vue-cli/e2e/methods/methods.ts @@ -41,15 +41,18 @@ export class VueCliMethods extends BaseMethods { isContain: false, }); - await this.clickElementWithText({ - selector: baseSelectors.tags.coreElements.button, - text: Constants.elementsText.vueCliApp.buttonsText.otherSectionButton, - }); + const buttonLocator = this.page + .locator(baseSelectors.tags.section) + .locator(baseSelectors.tags.coreElements.button) + .filter({ hasText: Constants.elementsText.vueCliApp.buttonsText.otherSectionButton }) + .first(); - await this.checkElementVisibility({ - parentSelector: baseSelectors.tags.section, - selector: baseSelectors.tags.code, - }); + const [dialog] = await Promise.all([ + this.page.waitForEvent('dialog', { timeout: 5_000 }), + buttonLocator.click(), + ]); + + await dialog.accept(); await this.checkElementWithTextPresence({ parentSelector: baseSelectors.tags.section, diff --git a/vue-cli/e2e/tests/consumerAppChecks.spec.ts b/vue-cli/e2e/tests/consumerAppChecks.spec.ts index a4c58cfc2ad..c812be9ecb9 100644 --- a/vue-cli/e2e/tests/consumerAppChecks.spec.ts +++ b/vue-cli/e2e/tests/consumerAppChecks.spec.ts @@ -88,10 +88,11 @@ test.describe('Vue CLI', () => { isContain: false, }); - await basePage.clickElementWithText({ + await basePage.checkBrowserAlertByText({ + parentSelector: baseSelectors.tags.section, selector: baseSelectors.tags.coreElements.button, - text: Constants.elementsText.vueCliApp.buttonsText.otherSectionButton, - wait: 1500, + alertMessage: Constants.commonPhrases.vueCliApp.otherAppAlertMessage, + index: 1, }); await basePage.compareInfoBetweenHosts({ diff --git a/vue-cli/e2e/tests/runAll.spec.ts b/vue-cli/e2e/tests/runAll.spec.ts deleted file mode 100644 index eb5c2fa62da..00000000000 --- a/vue-cli/e2e/tests/runAll.spec.ts +++ /dev/null @@ -1,3 +0,0 @@ -import './commonChecks.spec'; -import './consumerAppChecks.spec'; -import './otherAppChecks.spec'; diff --git a/vue-cli/other/src/components/MainComponent.vue b/vue-cli/other/src/components/MainComponent.vue index 319b6748942..43d551844c0 100644 --- a/vue-cli/other/src/components/MainComponent.vue +++ b/vue-cli/other/src/components/MainComponent.vue @@ -11,23 +11,97 @@ diff --git a/vue-cli/other/vue.config.js b/vue-cli/other/vue.config.js index 16b11b826f8..3990d17b9a7 100644 --- a/vue-cli/other/vue.config.js +++ b/vue-cli/other/vue.config.js @@ -1,6 +1,7 @@ const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin; module.exports = { + lintOnSave: false, publicPath: 'http://localhost:9001/', configureWebpack: { optimization: {