From 245c8faaf3506e121dbc51d95c32f1c8241e4e6b Mon Sep 17 00:00:00 2001 From: Irshad Ahmad Date: Fri, 11 Oct 2024 23:04:15 +0530 Subject: [PATCH] Add tests for WP Telegram --- .../components/rules/AddRuleGroup.tsx | 4 +- .../components/rules/RuleSetButtons.tsx | 6 +- plugins/wptelegram/dev.php | 2 +- .../js/settings/ui/p2tg/Destination.tsx | 1 + .../wptelegram-comments/settings-page.spec.ts | 13 +- .../wptelegram-login/settings-page.spec.ts | 27 +- .../specs/wptelegram-widget/public-ui.spec.ts | 4 +- .../wptelegram-widget/settings-page.spec.ts | 18 +- .../wptelegram/settings-page-p2tg.spec.ts | 413 ++++++++++++++++++ .../specs/wptelegram/settings-page.spec.ts | 290 ++++++++++++ test/e2e/utils/actions.ts | 50 ++- test/e2e/utils/editor/base-editor.ts | 78 ++++ test/e2e/utils/mocks.ts | 13 +- 13 files changed, 856 insertions(+), 63 deletions(-) create mode 100644 test/e2e/specs/wptelegram/settings-page-p2tg.spec.ts create mode 100644 test/e2e/specs/wptelegram/settings-page.spec.ts diff --git a/packages/js/shared/wptelegram-ui/components/rules/AddRuleGroup.tsx b/packages/js/shared/wptelegram-ui/components/rules/AddRuleGroup.tsx index 6abacffc..865f43a7 100644 --- a/packages/js/shared/wptelegram-ui/components/rules/AddRuleGroup.tsx +++ b/packages/js/shared/wptelegram-ui/components/rules/AddRuleGroup.tsx @@ -1,8 +1,8 @@ import { useCallback } from 'react'; import { Button } from '@wpsocio/adapters'; -import { AddIcon } from '@wpsocio/icons'; import { __ } from '@wpsocio/i18n'; +import { AddIcon } from '@wpsocio/icons'; import { DEFAULT_RULE } from './constants'; import type { RuleGroupProps } from './types'; @@ -16,7 +16,7 @@ export const AddRuleGroup: React.FC = ({ rulesArray }) => { return ( ); }; diff --git a/packages/js/shared/wptelegram-ui/components/rules/RuleSetButtons.tsx b/packages/js/shared/wptelegram-ui/components/rules/RuleSetButtons.tsx index 2754d521..49353949 100644 --- a/packages/js/shared/wptelegram-ui/components/rules/RuleSetButtons.tsx +++ b/packages/js/shared/wptelegram-ui/components/rules/RuleSetButtons.tsx @@ -1,8 +1,8 @@ import { useCallback } from 'react'; +import { Box, Flex, IconButton } from '@wpsocio/adapters'; import { __ } from '@wpsocio/i18n'; import { AddIcon, CloseIcon } from '@wpsocio/icons'; -import { Box, Flex, IconButton } from '@wpsocio/adapters'; import { DEFAULT_RULE } from './constants'; import type { RuleSetProps } from './types'; @@ -24,7 +24,7 @@ export const RuleSetButtons: React.FC = (props) => { } onClick={onAdd} title={__('Add')} @@ -33,7 +33,7 @@ export const RuleSetButtons: React.FC = (props) => { } onClick={onRemove} title={__('Remove')} diff --git a/plugins/wptelegram/dev.php b/plugins/wptelegram/dev.php index e33760eb..c79d8173 100644 --- a/plugins/wptelegram/dev.php +++ b/plugins/wptelegram/dev.php @@ -7,7 +7,7 @@ * @package WPTelegram * * @wordpress-plugin - * Plugin Name: WP Telegram Dev + * Plugin Name: WP Telegram * Plugin URI: https://t.me/WPTelegram * Description: ❌ DO NOT DELETE ❌ Development Environment for WP Telegram. Versioned high to avoid auto update. * Version: 999.999.999 diff --git a/plugins/wptelegram/js/settings/ui/p2tg/Destination.tsx b/plugins/wptelegram/js/settings/ui/p2tg/Destination.tsx index 137b3faa..3cc0c00e 100644 --- a/plugins/wptelegram/js/settings/ui/p2tg/Destination.tsx +++ b/plugins/wptelegram/js/settings/ui/p2tg/Destination.tsx @@ -28,6 +28,7 @@ export const Destination: React.FC = () => { name={`${PREFIX}.channels`} onBlur={onBlur} placeholder="@username" + addButtonLabel={__('Add Channel')} /> diff --git a/test/e2e/specs/wptelegram-comments/settings-page.spec.ts b/test/e2e/specs/wptelegram-comments/settings-page.spec.ts index b50e8227..b5d442af 100644 --- a/test/e2e/specs/wptelegram-comments/settings-page.spec.ts +++ b/test/e2e/specs/wptelegram-comments/settings-page.spec.ts @@ -30,19 +30,16 @@ test.describe('Settings', () => { test('Should not allow submission without code', async ({ page }) => { const code = page.getByLabel('Code'); - const validationMessage = await code.evaluate((element) => { - const input = element as HTMLTextAreaElement; - return input.validationMessage; - }); + const validationMessage = await code.evaluate( + (el: HTMLTextAreaElement) => el.validationMessage, + ); expect(validationMessage).toBe('Please fill out this field.'); // Should not show validation message before submission. expect(await page.content()).not.toContain('Code required'); - const saveButton = page.getByRole('button', { name: 'Save Changes' }); - - await saveButton.click(); + await actions.saveChangesButton.click(); // Press tab key to blur the code input to dismiss form validation tooltip. await page.keyboard.press('Tab'); @@ -78,7 +75,7 @@ test.describe('Settings', () => { const code = page.getByLabel('Code'); - code.waitFor(); + await code.waitFor(); expect(await code.inputValue()).toBe(''); }); diff --git a/test/e2e/specs/wptelegram-login/settings-page.spec.ts b/test/e2e/specs/wptelegram-login/settings-page.spec.ts index 927ae55c..6c8278b6 100644 --- a/test/e2e/specs/wptelegram-login/settings-page.spec.ts +++ b/test/e2e/specs/wptelegram-login/settings-page.spec.ts @@ -36,21 +36,18 @@ test.describe('Settings', () => { test('Should validate the bot token and username inputs', async ({ page, }) => { - const botToken = page.getByLabel('Bot Token'); + const botTokenField = page.getByLabel('Bot Token'); - const validationMessage = await botToken.evaluate((element) => { - const input = element as HTMLTextAreaElement; - return input.validationMessage; - }); + const validationMessage = await botTokenField.evaluate( + (el: HTMLInputElement) => el.validationMessage, + ); expect(validationMessage).toBe('Please fill out this field.'); // Should not show validation message before submission. expect(await page.content()).not.toContain('Bot Token required'); - const saveButton = page.getByRole('button', { name: 'Save Changes' }); - - await saveButton.click(); + await actions.saveChangesButton.click(); await page.keyboard.press('Tab'); @@ -62,7 +59,7 @@ test.describe('Settings', () => { expect(await page.content()).toContain('Bot Username required'); - await botToken.selectText(); + await botTokenField.selectText(); await page.keyboard.type('invalid-token'); @@ -86,7 +83,7 @@ test.describe('Settings', () => { }, }; // Mock the api call - await mocks.mockRequest(`bot${botToken}/getMe`, { json }); + const unmock = await mocks.mockRequest(`bot${botToken}/getMe`, { json }); const botTokenField = page.getByLabel('Bot Token'); const botUsernameField = page.getByLabel('Bot Username'); @@ -101,18 +98,20 @@ test.describe('Settings', () => { expect(await page.content()).not.toContain(result); - await actions.testBotTokenAndWait({ botToken }); + await actions.testBotTokenAndWait({ endpoint: `/bot${botToken}/getMe` }); expect(await page.content()).toContain(result); expect(await botUsernameField.inputValue()).toBe(json.result.username); + + await unmock(); }); test('Should handle the API call for invalid token', async ({ page }) => { const json = { ok: false, error_code: 401, description: 'Unauthorized' }; // Mock the api call - await mocks.mockRequest(`bot${botToken}/getMe`, { + const unmock = await mocks.mockRequest(`bot${botToken}/getMe`, { json, status: json.error_code, }); @@ -128,11 +127,13 @@ test.describe('Settings', () => { expect(await page.content()).not.toContain(result); - await actions.testBotTokenAndWait({ botToken }); + await actions.testBotTokenAndWait({ endpoint: `/bot${botToken}/getMe` }); expect(await page.content()).toContain(result); expect(await botUsernameField.inputValue()).toBe(''); + + await unmock(); }); test('That the bot username field is readonly by default', async ({ diff --git a/test/e2e/specs/wptelegram-widget/public-ui.spec.ts b/test/e2e/specs/wptelegram-widget/public-ui.spec.ts index 6e43d37b..3707c603 100644 --- a/test/e2e/specs/wptelegram-widget/public-ui.spec.ts +++ b/test/e2e/specs/wptelegram-widget/public-ui.spec.ts @@ -113,7 +113,7 @@ test.describe('Public UI', () => { const link = page.getByRole('link', { name: 'Join our channel' }); - await expect(link).toHaveCount(1); + await expect(link).toBeVisible(); const href = await link.getAttribute('href'); @@ -175,7 +175,7 @@ test.describe('Public UI', () => { for (const [text, href] of linksToAssert) { const link = page.getByRole('link', { name: text }); - await expect(link).toHaveCount(1); + await expect(link).toBeVisible(); const linkHref = await link.getAttribute('href'); diff --git a/test/e2e/specs/wptelegram-widget/settings-page.spec.ts b/test/e2e/specs/wptelegram-widget/settings-page.spec.ts index 40fca0bb..1352c129 100644 --- a/test/e2e/specs/wptelegram-widget/settings-page.spec.ts +++ b/test/e2e/specs/wptelegram-widget/settings-page.spec.ts @@ -102,25 +102,21 @@ test.describe('Settings', () => { const botTokenField = tabPanel.getByLabel('Bot Token'); - let validationMessage = await botTokenField.evaluate((element) => { - const input = element as HTMLInputElement; - return input.validationMessage; - }); + let validationMessage = await botTokenField.evaluate( + (el: HTMLInputElement) => el.validationMessage, + ); expect(validationMessage).toBeFalsy(); await tabPanel.getByLabel('Username').fill('SomeUsername'); - validationMessage = await botTokenField.evaluate((element) => { - const input = element as HTMLInputElement; - return input.validationMessage; - }); + validationMessage = await botTokenField.evaluate( + (el: HTMLInputElement) => el.validationMessage, + ); expect(validationMessage).toBe('Please fill out this field.'); - const saveButton = page.getByRole('button', { name: 'Save Changes' }); - - await saveButton.click(); + await actions.saveChangesButton.click(); await page.keyboard.press('Tab'); diff --git a/test/e2e/specs/wptelegram/settings-page-p2tg.spec.ts b/test/e2e/specs/wptelegram/settings-page-p2tg.spec.ts new file mode 100644 index 00000000..51f674e3 --- /dev/null +++ b/test/e2e/specs/wptelegram/settings-page-p2tg.spec.ts @@ -0,0 +1,413 @@ +import type { Locator, Page } from '@playwright/test'; +import { expect, test } from '@wordpress/e2e-test-utils-playwright'; +import { Actions } from '../../utils/actions.js'; +import { BlockEditor } from '../../utils/editor/block-editor.js'; +import { ClassicEditor } from '../../utils/editor/classic-editor.js'; +import { REST } from '../../utils/rest.js'; + +const botToken = '123456789:y7SdjUVdeSA8HRF3WmOqHAA-cOIiz9u04dC'; + +async function setupPostToTelegramSection(page: Page) { + await page.getByRole('tab', { name: 'Basics' }).click(); + + await page.getByLabel('Bot Token').fill(botToken); + + const button = page.getByRole('tab', { name: 'Post to Telegram' }); + + await button.click(); + + const buttonId = await button.getAttribute('id'); + + // Get the tab panel that the button controls + const tabPanel = page.locator( + `div[role="tabpanel"][aria-labelledby="${buttonId}"]`, + ); + + // Now let us activate the section + await tabPanel + .getByRole('checkbox', { name: 'Active' }) + .check({ force: true }); + + await tabPanel.getByRole('button', { name: 'Add Channel' }).click(); + + await tabPanel.getByPlaceholder('@username').last().fill('@WPTelegram'); + + return tabPanel; +} + +test.describe('Settings > P2TG', () => { + let actions: Actions; + let rest: REST; + let tabPanel: Locator; + + test.beforeAll(async ({ requestUtils }) => { + rest = new REST(requestUtils); + await requestUtils.activatePlugin('wp-telegram'); + }); + + test.beforeEach(async ({ admin, pageUtils, page }) => { + actions = new Actions(pageUtils); + + await rest.deleteOption('wptelegram'); + + await admin.visitAdminPage('admin.php', 'page=wptelegram'); + + tabPanel = await setupPostToTelegramSection(page); + }); + + test.afterAll(async ({ requestUtils }) => { + await requestUtils.deactivatePlugin('wp-telegram'); + await rest.deleteOption('wptelegram'); + }); + + test('Should disable the irrelevant fields', async () => { + // Fields that depend on the `{post_excerpt}` template tag + const templateField = tabPanel.getByLabel('Message Template'); + + const excerptFields = [ + 'Excerpt Source', + 'Excerpt Length', + 'Excerpt Newlines', + ]; + + for (const field of excerptFields) { + await expect(tabPanel.getByLabel(field)).not.toBeDisabled(); + } + + // Remove '{post_excerpt}' from the template + await templateField.fill('Some text'); + + for (const field of excerptFields) { + await expect(tabPanel.getByLabel(field)).toBeDisabled(); + } + + // Fields that depend on the featured image + const featuredImageField = tabPanel.getByLabel('Featured Image'); + + const imagePosition = tabPanel + .locator('input[type="radio"][name="p2tg.image_position"]') + .first(); + + const singleMessage = tabPanel.getByRole('checkbox', { + name: 'Single Message', + }); + + await expect(imagePosition).not.toBeDisabled(); + await expect(singleMessage).not.toBeDisabled(); + + // Now disable the featured image + await featuredImageField.uncheck({ force: true }); + + await expect(imagePosition).toBeDisabled(); + await expect(singleMessage).toBeDisabled(); + + // Fields that depend on "Disable link preview" + const disableLinkPreview = tabPanel.getByRole('checkbox', { + name: 'Disable link preview', + }); + const linkPreviewUrl = tabPanel.getByLabel('Link Preview URL'); + const showPreviewAboveText = tabPanel.getByRole('checkbox', { + name: 'Show preview above text', + }); + + await expect(linkPreviewUrl).not.toBeDisabled(); + await expect(showPreviewAboveText).not.toBeDisabled(); + + // Now disable the link preview + await disableLinkPreview.check({ force: true }); + + await expect(linkPreviewUrl).toBeDisabled(); + await expect(showPreviewAboveText).toBeDisabled(); + + // Fields that depend on the "Add Inline URL Button" setting + const inlineButtonSwitch = tabPanel.getByRole('checkbox', { + name: 'Add Inline URL Button', + }); + const inlineButtonText = tabPanel.getByLabel('Inline Button Text'); + const inlineButtonUrl = tabPanel.getByLabel('Inline Button URL'); + + expect(await inlineButtonSwitch.isChecked()).toBe(false); // By default, it is OFF + + await expect(inlineButtonText).toBeDisabled(); + await expect(inlineButtonUrl).toBeDisabled(); + + // Enable the inline button + await inlineButtonSwitch.check({ force: true }); + + await expect(inlineButtonText).not.toBeDisabled(); + await expect(inlineButtonUrl).not.toBeDisabled(); + }); + + test('Should show warnings for Single Message', async () => { + const singleMessage = tabPanel.getByRole('checkbox', { + name: 'Single Message', + }); + + const imagePositionAfter = tabPanel.getByRole('radio', { + name: 'After the Text', + }); + + const htmlStyle = tabPanel.getByRole('radio', { + name: 'HTML style', + }); + + const disableLinkPreview = tabPanel.getByRole('checkbox', { + name: 'Disable link preview', + }); + + const formattingWarning = 'Formatting should not be None.'; + const linkPreviewWarning = 'Disable link preview should not be enabled.'; + + await expect(tabPanel).not.toContainText(formattingWarning); + await expect(tabPanel).not.toContainText(linkPreviewWarning); + + await imagePositionAfter.check({ force: true }); + await expect(tabPanel).toContainText(formattingWarning); + await expect(tabPanel).not.toContainText(linkPreviewWarning); + + //Now we should have both the warnings + await disableLinkPreview.check({ force: true }); + await expect(tabPanel).toContainText(linkPreviewWarning); + await expect(tabPanel).toContainText(formattingWarning); + + // Disabling Single Message should remove the warnings + await singleMessage.uncheck({ force: true }); + await expect(tabPanel).not.toContainText(formattingWarning); + await expect(tabPanel).not.toContainText(linkPreviewWarning); + + await singleMessage.check({ force: true }); + + // Enabling HTML style should remove the formatting warning + await htmlStyle.check({ force: true }); + await expect(tabPanel).not.toContainText(formattingWarning); + await expect(tabPanel).toContainText(linkPreviewWarning); + }); + + test('Should clean up message template after saving changes', async ({ + page, + }) => { + const templateField = tabPanel.getByLabel('Message Template'); + + templateField.fill( + // trailing whitespaces,
and \n \n', + ); + + await actions.saveChangesAndWait({ + apiPath: '/wptelegram/v1/settings', + assertSaved: true, + }); + + const expectedValue = + '{post_title}\n\n{post_excerpt}\n\nView post'; + + // Should reflect immediately and after reload as well + expect(await templateField.inputValue()).toBe(expectedValue); + + await page.reload(); + + await templateField.waitFor(); + + expect(await templateField.inputValue()).toBe(expectedValue); + }); + + test('Should show/hide the post edit page UI', async ({ + admin, + pageUtils, + requestUtils, + editor, + page, + }) => { + const classicEditor = new ClassicEditor({ admin, pageUtils, requestUtils }); + const blockEditor = new BlockEditor({ + admin, + pageUtils, + requestUtils, + editor, + }); + + const editors = [ + { type: 'classic', editorInstance: classicEditor }, + { type: 'block', editorInstance: blockEditor }, + ]; + + async function assertTheUiToBe(status: 'visible' | 'hidden') { + for (const { type, editorInstance } of editors) { + await test.step(`Set up the ${type} editor`, async () => { + await editorInstance.setUp(); + }); + + await test.step(`Should be ${status} in ${type} editor`, async () => { + await editorInstance.createNewPost(); + + const sendToTelegram = page.getByRole('checkbox', { + name: 'Send to Telegram', + }); + + if (status === 'visible') { + await expect(sendToTelegram).toBeVisible(); + } else { + await expect(sendToTelegram).toBeHidden(); + } + }); + + await test.step(`Clean up the ${type} editor`, async () => { + await editorInstance.tearDown(); + }); + } + } + + // By default, the UI should be hidden when settings are not saved + await assertTheUiToBe('hidden'); + + await admin.visitAdminPage('admin.php', 'page=wptelegram'); + + tabPanel = await setupPostToTelegramSection(page); + + await actions.saveChangesAndWait({ + apiPath: '/wptelegram/v1/settings', + }); + + // Now the UI should be visible + await assertTheUiToBe('visible'); + + await admin.visitAdminPage('admin.php', 'page=wptelegram'); + + // Now lets us turn OFF post edit switch + tabPanel = await setupPostToTelegramSection(page); + + await tabPanel + .getByRole('checkbox', { name: 'Post edit switch' }) + .uncheck({ force: true }); + + await actions.saveChangesAndWait({ + apiPath: '/wptelegram/v1/settings', + }); + + // Now the UI should be hidden + await assertTheUiToBe('hidden'); + }); + + test('Should verify that the rules behave as expected', async ({ + admin, + pageUtils, + requestUtils, + editor, + page, + }) => { + const blockEditor = new BlockEditor({ + admin, + pageUtils, + requestUtils, + editor, + }); + + await blockEditor.addCategoriesAndTags(true); + + await admin.visitAdminPage('admin.php', 'page=wptelegram'); + + tabPanel = await setupPostToTelegramSection(page); + + const apiPath = '/wptelegram/v1/p2tg-rules'; + + await Promise.all([ + page.getByRole('button', { name: 'Add rule' }).click(), + actions.waitForApiResponse(apiPath), + ]); + + const combobox = page.getByRole('combobox', { name: 'Rule values' }); + + await combobox.fill('ABC'); + + const listbox = page.getByRole('listbox'); + + await actions.waitForApiResponse(apiPath); + + await listbox.waitFor(); + + const options = listbox.getByRole('option'); + + // Assert that there are only two options + expect(await options.count()).toBe(2); + expect(await listbox.textContent()).toContain('ABC Cat → ABC Child cat'); + + // Let us select an option + await options.filter({ hasText: 'ABC Cat → ABC Child cat' }).click(); + + await combobox.fill('ABC'); + + await actions.waitForApiResponse(apiPath); + + // Now there should be only one option + expect(await options.count()).toBe(1); + expect(await options.first().textContent()).not.toContain( + 'ABC Cat → ABC Child cat', + ); + expect(await options.first().textContent()).toContain('ABC Cat'); + // Let us select another option + await options.first().click(); + + await actions.saveChangesAndWait({ + apiPath: '/wptelegram/v1/settings', + }); + + await page.reload(); + + const valueContainer = page.locator('.react-select__value-container'); + + await valueContainer.waitFor(); + // We should two options selected + await expect( + valueContainer.locator('.react-select__multi-value'), + ).toHaveCount(2); + + // Let us add another rule to the group + await page.getByRole('button', { name: 'Add another rule' }).click(); + await page.getByRole('combobox', { name: 'Rule values' }).last().click(); + // The options should be readily available without making an API call + // The listbox should not show "Loading..." + + await expect(page.getByRole('listbox')).not.toContainText('Loading'); + + expect( + await page.getByRole('listbox').getByRole('option').count(), + ).toBeGreaterThan(0); + + // Now let us change the "Rule type" + + await page.getByLabel('Rule type').first().selectOption('Post Tag'); + + // The selected values should be cleared + await expect( + valueContainer.first().locator('.react-select__multi-value'), + ).toHaveCount(0); + + // Let us remove the first rule, which is now "Post Tag" + await page + .getByRole('button', { name: 'Remove this rule' }) + .first() + .click(); + + const ruleType = page.getByLabel('Rule type'); + + await expect(ruleType).toHaveCount(1); + + expect(ruleType).toHaveValue('category'); + + await page + .getByRole('combobox', { name: 'Rule values' }) + .fill('non-existent'); + + await expect(page.getByRole('listbox')).toContainText( + 'No options available', + ); + + // Now let us save the changes + // The rule should be removed because nothing is selected + await actions.saveChangesAndWait({ + apiPath: '/wptelegram/v1/settings', + }); + + await expect(valueContainer).toHaveCount(0); + }); +}); diff --git a/test/e2e/specs/wptelegram/settings-page.spec.ts b/test/e2e/specs/wptelegram/settings-page.spec.ts new file mode 100644 index 00000000..307c2ded --- /dev/null +++ b/test/e2e/specs/wptelegram/settings-page.spec.ts @@ -0,0 +1,290 @@ +import { expect, test } from '@wordpress/e2e-test-utils-playwright'; +import { Actions } from '../../utils/actions.js'; +import { Mocks } from '../../utils/mocks.js'; +import { REST } from '../../utils/rest.js'; + +test.describe('Settings', () => { + let actions: Actions; + let rest: REST; + let mocks: Mocks; + + const botToken = '123456789:y7SdjUVdeSA8HRF3WmOqHAA-cOIiz9u04dC'; + + test.beforeAll(async ({ requestUtils }) => { + rest = new REST(requestUtils); + await requestUtils.activatePlugin('wp-telegram'); + }); + + test.beforeEach(async ({ admin, pageUtils }) => { + actions = new Actions(pageUtils); + mocks = new Mocks(pageUtils); + + await rest.deleteOption('wptelegram'); + await admin.visitAdminPage('admin.php', 'page=wptelegram'); + }); + + test.afterAll(async ({ requestUtils }) => { + await requestUtils.deactivatePlugin('wp-telegram'); + await rest.deleteOption('wptelegram'); + }); + + test('Should have instructions', async ({ page }) => { + expect(await page.content()).toContain('INSTRUCTIONS!'); + }); + + test('Should not allow submission without bot token', async ({ page }) => { + const botTokenField = page.getByLabel('Bot Token'); + + const validationMessage = await botTokenField.evaluate( + (el: HTMLInputElement) => el.validationMessage, + ); + + expect(validationMessage).toBe('Please fill out this field.'); + + // Should not show validation message before submission. + expect(await page.content()).not.toContain('Bot Token required'); + + await actions.saveChangesButton.click(); + + // Press tab key to blur the code input to dismiss form validation tooltip. + await page.keyboard.press('Tab'); + + expect(await page.content()).toContain('Bot Token required'); + }); + + test('Should not allow submission with invalid bot token', async ({ + page, + }) => { + const code = page.getByLabel('Bot Token'); + + await code.selectText(); + + await page.keyboard.type('invalid-token'); + + // Press tab key to blur the code input to trigger validation. + await page.keyboard.press('Tab'); + + expect(await page.content()).toContain('Invalid Bot Token'); + }); + + test('Should save the changes', async ({ page }) => { + await page.getByLabel('Bot Token').fill(botToken); + + await actions.saveChangesAndWait({ + apiPath: '/wptelegram/v1/settings', + assertSaved: true, + }); + + // Reload the page and wait + await page.reload(); + + const botTokenField = page.getByLabel('Bot Token'); + + await botTokenField.waitFor(); + + expect(await botTokenField.inputValue()).toBe(botToken); + }); + + test('Should validate the bot token from API and fill username', async ({ + page, + }) => { + const json = { + ok: true, + result: { + id: 123, + first_name: 'The E2E Test Bot', + username: 'E2ETestBot', + }, + }; + // Mock the api call + await mocks.mockRequest('/wptelegram-bot/v1/getMe', { json }); + + const botTokenField = page.getByLabel('Bot Token'); + const botUsernameField = page.getByLabel('Bot Username'); + + expect(await botUsernameField.inputValue()).toBe(''); + + await botTokenField.selectText(); + + await page.keyboard.type(botToken); + + const result = `${json.result.first_name} (@${json.result.username})`; + + expect(await page.content()).not.toContain(result); + + await actions.testBotTokenAndWait(); + + expect(await page.content()).toContain(result); + + expect(await botUsernameField.inputValue()).toBe(json.result.username); + }); + + test('Should handle the API call for invalid token', async ({ page }) => { + const json = { ok: false, error_code: 401, description: 'Unauthorized' }; + + // Mock the api call + const unmock = await mocks.mockRequest('/wptelegram-bot/v1/getMe', { + json, + status: json.error_code, + }); + + const botTokenField = page.getByLabel('Bot Token'); + const botUsernameField = page.getByLabel('Bot Username'); + + await botTokenField.selectText(); + + await page.keyboard.type(botToken); + + const result = 'Error: 401 (Unauthorized)'; + + expect(await page.content()).not.toContain(result); + + await actions.testBotTokenAndWait(); + + expect(await page.content()).toContain(result); + + expect(await botUsernameField.inputValue()).toBe(''); + + await unmock(); + }); + + test('That the bot username field is readonly by default', async ({ + page, + }) => { + const botUsernameField = page.getByLabel('Bot Username'); + + await expect(botUsernameField).toHaveAttribute('readonly'); + + await botUsernameField.dblclick(); + + await expect(botUsernameField).not.toHaveAttribute('readonly'); + }); + + test('Should hide the fields for sections that require bot token', async ({ + page, + }) => { + const tabFields: Array<[tabName: string, fields: Array]> = [ + ['Post to Telegram', ['Message Template']], + ['Private Notifications', ['Message Template']], + ]; + + // Let us touch all the fields and then save the settings + for (const [tab, fields] of tabFields) { + const button = page.getByRole('tab', { name: tab }); + + await button.click(); + + const buttonId = await button.getAttribute('id'); + + // Get the tab panel that the button controls + const tabPanel = page.locator( + `div[role="tabpanel"][aria-labelledby="${buttonId}"]`, + ); + + // Now let us activate the section + await tabPanel + .getByRole('checkbox', { name: 'Active' }) + .check({ force: true }); + + // Assert that fields are not visible + for (const field of fields) { + await expect(tabPanel.getByLabel(field)).toHaveCount(0); + } + } + }); + + test('Should not allow saving the active sections with required fields', async ({ + page, + }) => { + await page.getByLabel('Bot Token').fill(botToken); + + const button = page.getByRole('tab', { name: 'Post to Telegram' }); + + await button.click(); + + const buttonId = await button.getAttribute('id'); + + // Get the tab panel that the button controls + const tabPanel = page.locator( + `div[role="tabpanel"][aria-labelledby="${buttonId}"]`, + ); + + // Now let us activate the section + await tabPanel + .getByRole('checkbox', { name: 'Active' }) + .check({ force: true }); + + await actions.saveChangesButton.click(); + + expect(await page.content()).toContain('At least one channel is required.'); + + await tabPanel.getByRole('button', { name: 'Add Channel' }).click(); + + await tabPanel.getByPlaceholder('@username').fill('@WPTelegram'); + + await actions.saveChangesAndWait({ + apiPath: '/wptelegram/v1/settings', + assertSaved: true, + }); + }); + + test.skip('Should display proxy options conditionally', async ({ page }) => { + await page.getByLabel('Bot Token').fill(botToken); + + const button = page.getByRole('tab', { name: 'Proxy' }); + + await button.click(); + + const buttonId = await button.getAttribute('id'); + + // Get the tab panel that the button controls + const tabPanel = page.locator( + `div[role="tabpanel"][aria-labelledby="${buttonId}"]`, + ); + + // Now let us activate the section + await tabPanel + .getByRole('checkbox', { name: 'Active' }) + .check({ force: true }); + + // Cloudflare proxy should be checked by default + await expect( + tabPanel.getByRole('radio', { name: 'Cloudflare worker' }), + ).toBeChecked(); + await expect( + tabPanel.getByRole('textbox', { name: 'Cloudflare worker URL' }), + ).toBeVisible(); + await expect( + tabPanel.getByRole('textbox', { name: 'Google Script URL' }), + ).toHaveCount(0); + + // Let us change the proxy type to "Google Script" + await tabPanel.getByRole('radio', { name: 'Google Script' }).check({ + force: true, + }); + await expect( + tabPanel.getByRole('textbox', { name: 'Google Script URL' }), + ).toBeVisible(); + await expect( + tabPanel.getByRole('textbox', { name: 'Cloudflare worker URL' }), + ).toHaveCount(0); + + // Let us change the proxy type to "PHP Proxy" + await tabPanel.getByRole('radio', { name: 'PHP Proxy' }).check({ + force: true, + }); + await expect( + tabPanel.getByRole('textbox', { name: 'PHP Proxy URL' }), + ).toBeVisible(); + await expect( + tabPanel.getByRole('textbox', { name: 'Google Script URL' }), + ).toHaveCount(0); + + const proxyFields = ['Proxy Host', 'Proxy Port', 'Username', 'Password']; + + // Let us touch all the fields and then save the settings + for (const field of proxyFields) { + await tabPanel.getByLabel(field).focus(); + } + }); +}); diff --git a/test/e2e/utils/actions.ts b/test/e2e/utils/actions.ts index a8692661..e274953b 100644 --- a/test/e2e/utils/actions.ts +++ b/test/e2e/utils/actions.ts @@ -8,26 +8,20 @@ export class Actions { this.page = pageUtils.page; } - async saveChangesAndWait({ - apiPath, - assertSaved = false, - }: { apiPath: string; assertSaved?: boolean }) { - const saveButton = this.page.getByRole('button', { + get saveChangesButton() { + return this.page.getByRole('button', { name: 'Save Changes', exact: true, }); + } + async saveChangesAndWait({ + apiPath, + assertSaved = false, + }: { apiPath: string; assertSaved?: boolean }) { await Promise.all([ - saveButton.click(), - this.page.waitForResponse((resp) => { - const url = resp.url(); - - return ( - url.includes(apiPath) || - // API path can be encoded in the URL as `rest_route`. - url.includes(encodeURIComponent(apiPath)) - ); - }), + this.saveChangesButton.click(), + this.waitForApiResponse(apiPath), ]); if (assertSaved) { @@ -35,9 +29,21 @@ export class Actions { } } - async testBotTokenAndWait({ botToken }: { botToken: string }) { - const apiPath = `/bot${botToken}/getMe`; + async waitForApiResponse(apiPath: string) { + return await this.page.waitForResponse((resp) => { + const url = resp.url(); + return ( + url.includes(apiPath) || + // API path can be URL encoded + url.includes(encodeURIComponent(apiPath)) + ); + }); + } + + async testBotTokenAndWait({ + endpoint = '/wptelegram-bot/v1/getMe', + }: { endpoint?: string } = {}) { const testButton = this.page.getByRole('button', { name: 'Test Token', exact: true, @@ -45,7 +51,7 @@ export class Actions { return await Promise.all([ testButton.click(), - this.page.waitForResponse((resp) => resp.url().includes(apiPath)), + this.waitForApiResponse(endpoint), ]); } @@ -54,11 +60,15 @@ export class Actions { name: 'Notifications', }); - const notificationShown = notifications.getByRole('status'); + const notificationShown = notifications.locator('div[role="status"]', { + has: this.page.locator('span[data-status="success"]'), + }); await notificationShown.waitFor(); - expect(await this.page.content()).toContain('Changes saved successfully.'); + expect(await notificationShown.textContent()).toContain( + 'Changes saved successfully.', + ); } async logout() { diff --git a/test/e2e/utils/editor/base-editor.ts b/test/e2e/utils/editor/base-editor.ts index 6e876e30..0132cc6e 100644 --- a/test/e2e/utils/editor/base-editor.ts +++ b/test/e2e/utils/editor/base-editor.ts @@ -41,4 +41,82 @@ export abstract class BaseEditor { abstract insertShortCode(shortCode: string): Promise; abstract clearContent(): Promise; + + async addCategoriesAndTags(deleteExisting = false) { + const taxonomyTerms = { + category: [ + { name: 'ABC Cat' }, + { name: 'DEF Cat' }, + { name: 'GHI Cat' }, + { name: 'XYZ Cat' }, + { name: 'جبا Cat' }, + { name: 'قط means Cat' }, + { name: 'ABC Child cat', parent: 'ABC Cat' }, + { name: 'DEF Child cat', parent: 'DEF Cat' }, + { name: 'XYZ Child Cat', parent: 'XYZ Cat' }, + ], + post_tag: [ + { name: 'ABC Tag' }, + { name: 'DEF Tag' }, + { name: 'GHI Tag' }, + { name: 'XYZ Tag' }, + { name: 'جبا Tag' }, + { name: 'جبل is mountain', parent: '' }, + ], + }; + + for (const [taxonomy, terms] of Object.entries(taxonomyTerms)) { + await this.admin.visitAdminPage('edit-tags.php', `taxonomy=${taxonomy}`); + + if (deleteExisting) { + while ( + (await this.page.locator('table.wp-list-table tbody tr').count()) > 1 + ) { + await this.page + .getByRole('checkbox', { name: 'Select All' }) + .first() + .check(); + + const bulkActions = this.page + .locator('div.actions.bulkactions') + .first(); + + await bulkActions + .getByLabel('Select bulk action') + .selectOption('Delete'); + + await Promise.all([ + bulkActions.locator('input[type="submit"][value="Apply"]').click(), + this.page.waitForResponse((resp) => { + return ( + resp.url().includes('wp-admin/edit-tags.php') && + resp.request().method() === 'POST' && + (resp.request().postData() || '').includes('action=delete') + ); + }), + ]); + } + } + + for (const { name, parent } of terms) { + const form = this.page.locator('form#addtag'); + + await form.getByLabel('Name').fill(name); + + if (parent) { + await form.getByLabel('Parent Category').selectOption(parent); + } + + await Promise.all([ + form.locator('input[type="submit"]').click(), + this.page.waitForResponse((resp) => { + return ( + resp.url().includes('wp-admin/admin-ajax.php') && + (resp.request().postData() || '').includes('action=add-tag') + ); + }), + ]); + } + } + } } diff --git a/test/e2e/utils/mocks.ts b/test/e2e/utils/mocks.ts index b672c3d7..75ccee09 100644 --- a/test/e2e/utils/mocks.ts +++ b/test/e2e/utils/mocks.ts @@ -12,13 +12,20 @@ export class Mocks { urlSubstr: string, options: Parameters[0], ) { - const predicate = (url: URL) => url.pathname.includes(urlSubstr); + const predicate = (url: URL) => { + return ( + url.href.includes(urlSubstr) || + // API path can be encoded in the URL as `rest_route`. + url.href.includes(encodeURIComponent(urlSubstr)) + ); + }; + const handler = async (route: Route) => await route.fulfill(options); await this.page.route(predicate, handler); - return () => { - this.page.unroute(predicate, handler); + return async () => { + await this.page.unroute(predicate, handler); }; }