diff --git a/.eslintignore b/.eslintignore index 29aafbf9250..f2e3b91ce3b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,7 @@ .idea/ .next/ build/ +dist/ coverage/ node_modules/ *.min.js diff --git a/.prettierignore b/.prettierignore index 1262ced8cd0..57d241b55ef 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,7 @@ .idea/ .next/ build/ +dist/ coverage/ node_modules/ *.min.css diff --git a/.stylelintignore b/.stylelintignore index c64e659b075..3522cdc599b 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -1,6 +1,7 @@ .next build/ coverage/ +dist/ node_modules/ *.min.css # Artifacts can leak between jobs on self-hosted runners diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/TopicServicePermissionTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/TopicServicePermissionTests.cs index 9cd0fd493ab..532916bf098 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/TopicServicePermissionTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/TopicServicePermissionTests.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using AutoMapper; using GovUk.Education.ExploreEducationStatistics.Admin.Security; @@ -100,16 +101,17 @@ public void DeleteTopic() } private TopicService SetupTopicService( - ContentDbContext contentContext = null, - StatisticsDbContext statisticsContext = null, - PersistenceHelper persistenceHelper = null, - IMapper mapper = null, - IUserService userService = null, - IReleaseSubjectRepository releaseSubjectRepository = null, - IReleaseDataFileService releaseDataFileService = null, - IReleaseFileService releaseFileService = null, - IPublishingService publishingService = null, - IMethodologyService methodologyService = null) + ContentDbContext? contentContext = null, + StatisticsDbContext? statisticsContext = null, + PersistenceHelper? persistenceHelper = null, + IMapper? mapper = null, + IUserService? userService = null, + IReleaseSubjectRepository? releaseSubjectRepository = null, + IReleaseDataFileService? releaseDataFileService = null, + IReleaseFileService? releaseFileService = null, + IPublishingService? publishingService = null, + IMethodologyService? methodologyService = null, + IReleasePublishingStatusRepository? releasePublishingStatusRepository = null) { return new TopicService( Mock.Of(), @@ -123,7 +125,8 @@ private TopicService SetupTopicService( releaseFileService ?? Mock.Of(), publishingService ?? Mock.Of(), methodologyService ?? Mock.Of(), - Mock.Of() + Mock.Of(), + releasePublishingStatusRepository ?? Mock.Of() ); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/TopicServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/TopicServiceTests.cs index e7363f7ea33..6b77bc6c101 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/TopicServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/TopicServiceTests.cs @@ -413,6 +413,7 @@ public async Task DeleteTopic() var methodologyService = new Mock(Strict); var publishingService = new Mock(Strict); var cacheService = new Mock(Strict); + var releasePublishingStatusRepository = new Mock(Strict); await using (var contentContext = DbUtils.InMemoryApplicationDbContext(contextId)) await using (var statisticsContext = InMemoryStatisticsDbContext(contextId)) @@ -425,7 +426,8 @@ public async Task DeleteTopic() releaseSubjectRepository: releaseSubjectRepository.Object, methodologyService: methodologyService.Object, publishingService: publishingService.Object, - cacheService: cacheService.Object); + cacheService: cacheService.Object, + releasePublishingStatusRepository: releasePublishingStatusRepository.Object); releaseDataFileService .Setup(s => s.DeleteAll(releaseId, true)) @@ -452,13 +454,18 @@ public async Task DeleteTopic() ItIs.DeepEqualTo(new PrivateReleaseContentFolderCacheKey(releaseId)))) .Returns(Task.CompletedTask); + releasePublishingStatusRepository.Setup(mock => + mock.RemovePublisherReleaseStatuses(new List{ release.Id })) + .Returns(Task.CompletedTask); + var result = await service.DeleteTopic(topic.Id); VerifyAllMocks(releaseDataFileService, releaseFileService, releaseSubjectRepository, methodologyService, publishingService, - cacheService); + cacheService, + releasePublishingStatusRepository); result.AssertRight(); @@ -558,6 +565,7 @@ public async Task DeleteTopic_ReleaseVersionsDeletedInCorrectOrder() var releaseSubjectRepository = new Mock(Strict); var publishingService = new Mock(Strict); var cacheService = new Mock(Strict); + var releasePublishingStatusRepository = new Mock(Strict); await using (var contentContext = DbUtils.InMemoryApplicationDbContext(contextId)) await using (var statisticsContext = InMemoryStatisticsDbContext(contextId)) @@ -569,7 +577,8 @@ public async Task DeleteTopic_ReleaseVersionsDeletedInCorrectOrder() releaseFileService: releaseFileService.Object, releaseSubjectRepository: releaseSubjectRepository.Object, publishingService: publishingService.Object, - cacheService: cacheService.Object); + cacheService: cacheService.Object, + releasePublishingStatusRepository: releasePublishingStatusRepository.Object); var releaseDataFileDeleteSequence = new MockSequence(); @@ -608,13 +617,18 @@ public async Task DeleteTopic_ReleaseVersionsDeletedInCorrectOrder() publishingService.Setup(s => s.TaxonomyChanged()) .ReturnsAsync(Unit.Instance); + releasePublishingStatusRepository.Setup(mock => + mock.RemovePublisherReleaseStatuses(releaseIdsInExpectedDeleteOrder)) + .Returns(Task.CompletedTask); + var result = await service.DeleteTopic(topicId); VerifyAllMocks( releaseDataFileService, releaseFileService, releaseSubjectRepository, publishingService, - cacheService); + cacheService, + releasePublishingStatusRepository); result.AssertRight(); @@ -872,6 +886,7 @@ public async Task DeleteTopic_OtherTopicsUnaffected() var methodologyService = new Mock(Strict); var publishingService = new Mock(Strict); var cacheService = new Mock(Strict); + var releasePublishingStatusRepository = new Mock(Strict); await using (var contentContext = DbUtils.InMemoryApplicationDbContext(contextId)) await using (var statisticsContext = InMemoryStatisticsDbContext(contextId)) @@ -884,7 +899,8 @@ public async Task DeleteTopic_OtherTopicsUnaffected() releaseSubjectRepository: releaseSubjectRepository.Object, methodologyService: methodologyService.Object, publishingService: publishingService.Object, - cacheService: cacheService.Object); + cacheService: cacheService.Object, + releasePublishingStatusRepository: releasePublishingStatusRepository.Object); releaseDataFileService .Setup(s => s.DeleteAll(releaseId, true)) @@ -911,13 +927,18 @@ public async Task DeleteTopic_OtherTopicsUnaffected() ItIs.DeepEqualTo(new PrivateReleaseContentFolderCacheKey(releaseId)))) .Returns(Task.CompletedTask); + releasePublishingStatusRepository.Setup(mock => + mock.RemovePublisherReleaseStatuses(new List{ releaseId })) + .Returns(Task.CompletedTask); + var result = await service.DeleteTopic(topic.Id); VerifyAllMocks(releaseDataFileService, releaseFileService, releaseSubjectRepository, methodologyService, publishingService, - cacheService); + cacheService, + releasePublishingStatusRepository); result.AssertRight(); @@ -942,6 +963,7 @@ private static TopicService SetupTopicService( IPublishingService? publishingService = null, IMethodologyService? methodologyService = null, IBlobCacheService? cacheService = null, + IReleasePublishingStatusRepository? releasePublishingStatusRepository = null, bool enableThemeDeletion = true) { var configuration = @@ -959,7 +981,8 @@ private static TopicService SetupTopicService( releaseFileService ?? Mock.Of(Strict), publishingService ?? Mock.Of(Strict), methodologyService ?? Mock.Of(Strict), - cacheService ?? Mock.Of(Strict) + cacheService ?? Mock.Of(Strict), + releasePublishingStatusRepository ?? Mock.Of(Strict) ); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/IReleasePublishingStatusRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/IReleasePublishingStatusRepository.cs index 764d908541b..843c999bd40 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/IReleasePublishingStatusRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/IReleasePublishingStatusRepository.cs @@ -8,5 +8,7 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces public interface IReleasePublishingStatusRepository { Task> GetAllByOverallStage(Guid releaseId, params ReleasePublishingStatusOverallStage[] overallStages); + + Task RemovePublisherReleaseStatuses(List releaseIds); } -} \ No newline at end of file +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleasePublishingStatusRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleasePublishingStatusRepository.cs index afb6574e973..b8155af27d1 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleasePublishingStatusRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleasePublishingStatusRepository.cs @@ -3,7 +3,9 @@ using System.Linq; using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Data.Model; using GovUk.Education.ExploreEducationStatistics.Publisher.Model; using Microsoft.Azure.Cosmos.Table; using static GovUk.Education.ExploreEducationStatistics.Common.TableStorageTableNames; @@ -47,5 +49,35 @@ public Task> GetAllByOverallStage(Guid rele var query = new TableQuery().Where(filter); return _publisherTableStorageService.ExecuteQueryAsync(PublisherReleaseStatusTableName, query); } + + public async Task RemovePublisherReleaseStatuses(List releaseIds) + { + if (releaseIds.IsNullOrEmpty()) + { + // Return early as we want to do nothing in this case - without this, + // `filter` will be string.Empty and the query returns all table entities + return; + } + + var filter = string.Empty; + foreach (var releaseId in releaseIds) + { + var newFilter = TableQuery.GenerateFilterCondition(nameof(ReleasePublishingStatus.PartitionKey), + QueryComparisons.Equal, releaseId.ToString()); + + filter = filter == string.Empty + ? newFilter + : TableQuery.CombineFilters(filter, TableOperators.Or, newFilter); + } + + var cloudTable = _publisherTableStorageService.GetTable(PublisherReleaseStatusTableName); + var query = new TableQuery().Where(filter); + var releaseStatusesToRemove = cloudTable.ExecuteQuery(query); + + foreach (var releaseStatus in releaseStatusesToRemove) + { + await cloudTable.ExecuteAsync(TableOperation.Delete(releaseStatus)); + } + } } -} \ No newline at end of file +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/TopicService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/TopicService.cs index 40a35c999ac..200e32db74e 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/TopicService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/TopicService.cs @@ -11,7 +11,6 @@ using GovUk.Education.ExploreEducationStatistics.Admin.Validators; using GovUk.Education.ExploreEducationStatistics.Admin.ViewModels; using GovUk.Education.ExploreEducationStatistics.Common.Cache; -using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Model; using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces.Security; @@ -43,6 +42,7 @@ public class TopicService : ITopicService private readonly IPublishingService _publishingService; private readonly IMethodologyService _methodologyService; private readonly IBlobCacheService _cacheService; + private readonly IReleasePublishingStatusRepository _releasePublishingStatusRepository; private readonly bool _topicDeletionAllowed; public TopicService( @@ -57,7 +57,8 @@ public TopicService( IReleaseFileService releaseFileService, IPublishingService publishingService, IMethodologyService methodologyService, - IBlobCacheService cacheService) + IBlobCacheService cacheService, + IReleasePublishingStatusRepository releasePublishingStatusRepository) { _contentContext = contentContext; _statisticsContext = statisticsContext; @@ -70,6 +71,7 @@ public TopicService( _publishingService = publishingService; _methodologyService = methodologyService; _cacheService = cacheService; + _releasePublishingStatusRepository = releasePublishingStatusRepository; _topicDeletionAllowed = configuration.GetValue("enableThemeDeletion"); } @@ -229,12 +231,18 @@ private async Task> DeleteReleasesForPublications(IEn var releaseIdsInDeleteOrder = VersionedEntityDeletionOrderUtil .Sort(releaseIdsToDelete) - .Select(ids => Guid.Parse(ids.Id)); + .Select(ids => Guid.Parse(ids.Id)) + .ToList(); + + // Delete release entries in the Azure Storage ReleaseStatus table - if not it will attempt to publish + // deleted releases that were left scheduled + await _releasePublishingStatusRepository.RemovePublisherReleaseStatuses(releaseIdsInDeleteOrder); return await releaseIdsInDeleteOrder .Select(DeleteContentAndStatsRelease) .OnSuccessAll() .OnSuccessVoid(); + } private async Task> DeletePublications(IEnumerable publicationIds) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Api/Controllers/TableBuilderController.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Api/Controllers/TableBuilderController.cs index fcab11c8227..58e4be855c0 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Api/Controllers/TableBuilderController.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Api/Controllers/TableBuilderController.cs @@ -104,16 +104,22 @@ await _tableBuilderService.QueryToCsvStream( // both in order to support legacy URLs in bookmarks and in content, but also to remain consistent with the equivalent // endpoint in the Admin API, which does require the Release Id in order to differentiate between different // DataBlockVersions rather than simply picking the latest published one. - [ResponseCache(Duration = 300)] [HttpGet("tablebuilder/release/{releaseId:guid}/data-block/{dataBlockParentId:guid}")] public async Task> QueryForTableBuilderResult( Guid dataBlockParentId) { - return await GetLatestPublishedDataBlockVersion(dataBlockParentId) + var actionResult = await GetLatestPublishedDataBlockVersion(dataBlockParentId) .OnSuccessDo(dataBlockVersion => this .CacheWithLastModifiedAndETag(lastModified: dataBlockVersion.Published, ApiVersion)) .OnSuccess(GetDataBlockTableResult) .HandleFailuresOrOk(); + + if (actionResult.Result is not NotFoundResult) + { + Response.Headers["Cache-Control"] = "public,max-age=300"; + } + + return actionResult; } [HttpGet("tablebuilder/fast-track/{dataBlockParentId:guid}")] diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs index 908188fd429..8b51ab2ea35 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs @@ -108,14 +108,11 @@ await releaseIdsToUpdate } }); - var publicationSlugs = prePublishingStagesComplete - .Select(status => status.PublicationSlug) - .Distinct(); - var directlyRelatedPublicationIds = await _contentDbContext - .Publications - .Where(p => publicationSlugs.Contains(p.Slug)) - .Select(p => p.Id) + .Releases + .Where(r => releaseIdsToUpdate.Contains(r.Id)) + .Select(r => r.PublicationId) + .Distinct() .ToListAsync(); await directlyRelatedPublicationIds diff --git a/tests/playwright-tests/.env.example b/tests/playwright-tests/.env.example index d74141e5eef..3c47ee68e63 100644 --- a/tests/playwright-tests/.env.example +++ b/tests/playwright-tests/.env.example @@ -1 +1,4 @@ -PUBLIC_URL=http://localhost:3000 # URL of the public frontend \ No newline at end of file +PUBLIC_URL=http://localhost:3000 # URL of the public frontend +ADMIN_URL=https://localhost:5021 # URL for the Admin frontend +ADMIN_EMAIL=ADMIN_EMAIL # BAU1 user email +ADMIN_PASSWORD=ADMIN_PASSWORD # BAU1 user password \ No newline at end of file diff --git a/tests/playwright-tests/admin/azpage/AzureLoginPage.ts b/tests/playwright-tests/admin/azpage/AzureLoginPage.ts new file mode 100644 index 00000000000..ad9a9138590 --- /dev/null +++ b/tests/playwright-tests/admin/azpage/AzureLoginPage.ts @@ -0,0 +1,33 @@ +import { Locator, Page } from '@playwright/test'; +import environment from '../../utils/env'; + +export default class AzureLoginPage { + readonly page: Page; + readonly emailAddress: Locator; + readonly nextButton: Locator; + readonly password: Locator; + readonly signInButton: Locator; + readonly noButton: Locator; + + constructor(page: Page) { + this.page = page; + // Locators + this.emailAddress = page.locator( + 'input[placeholder="Email, phone, or Skype"]', + ); + this.nextButton = page.locator('input[type="submit"]'); + this.password = page.locator('input[type="password"]'); + this.signInButton = page.locator('input[id="idSIButton9"]'); + this.noButton = page.locator('input[id="idBtn_Back"]'); + } + + async doSignIn() { + await this.emailAddress.fill(environment.ADMIN_EMAIL); + await this.nextButton.click(); + await this.password.waitFor({ state: 'visible' }); + await this.password.fill(environment.ADMIN_PASSWORD); + await this.signInButton.click(); + await this.noButton.waitFor({ state: 'visible' }); + await this.noButton.click(); + } +} diff --git a/tests/playwright-tests/admin/pages/AdminPage.ts b/tests/playwright-tests/admin/pages/AdminPage.ts new file mode 100644 index 00000000000..8507d89d607 --- /dev/null +++ b/tests/playwright-tests/admin/pages/AdminPage.ts @@ -0,0 +1,20 @@ +import { Locator, Page } from '@playwright/test'; + +export default class AdminPage { + readonly page: Page; + readonly signInButton: Locator; + readonly manageThemesTopicLink: Locator; + + constructor(page: Page) { + this.page = page; + // Locators + this.signInButton = page.locator('button[id="signin-button"]'); + this.manageThemesTopicLink = page.locator( + '//a[text()="manage themes and topics" ]', + ); + } + + async clickSignIn() { + this.signInButton.click(); + } +} diff --git a/tests/playwright-tests/admin/pages/CreateThemePage.ts b/tests/playwright-tests/admin/pages/CreateThemePage.ts new file mode 100644 index 00000000000..b574155a51e --- /dev/null +++ b/tests/playwright-tests/admin/pages/CreateThemePage.ts @@ -0,0 +1,22 @@ +import { Locator, Page } from '@playwright/test'; + +export default class CreateThemePage { + readonly page: Page; + readonly themeTitle: Locator; + readonly themeSummary: Locator; + readonly saveThemeButton: Locator; + + constructor(page: Page) { + this.page = page; + // Locators + this.themeTitle = page.locator('input[id="themeForm-title"]'); + this.themeSummary = page.locator('input[id="themeForm-summary"]'); + this.saveThemeButton = page.locator('//button[text()="Save theme"]'); + } + + async doCreateTheme(title: string, summary: string) { + await this.themeTitle.fill(title); + await this.themeSummary.fill(summary); + await this.saveThemeButton.click(); + } +} diff --git a/tests/playwright-tests/admin/pages/ThemesPage.ts b/tests/playwright-tests/admin/pages/ThemesPage.ts new file mode 100644 index 00000000000..88a32032f0a --- /dev/null +++ b/tests/playwright-tests/admin/pages/ThemesPage.ts @@ -0,0 +1,20 @@ +import { Locator, Page } from '@playwright/test'; + +export default class ThemesPage { + readonly page: Page; + readonly createThemeLink: Locator; + readonly themeTitle: (text: string) => Locator; + + constructor(page: Page) { + this.page = page; + // Locators + this.createThemeLink = page.locator('//a[text()="Create theme"]'); + this.themeTitle = (text: string) => + page.locator(`//span[text()="${text}"]`); + } + + async checkThemeIsDisplayed(text: string) { + const themeTile = this.themeTitle(text); + await themeTile.isVisible(); + } +} diff --git a/tests/playwright-tests/general-public/pages/glossary-page.ts b/tests/playwright-tests/general-public/pages/GlossaryPage.ts similarity index 91% rename from tests/playwright-tests/general-public/pages/glossary-page.ts rename to tests/playwright-tests/general-public/pages/GlossaryPage.ts index eb4a4846b56..ebc2fe9add1 100644 --- a/tests/playwright-tests/general-public/pages/glossary-page.ts +++ b/tests/playwright-tests/general-public/pages/GlossaryPage.ts @@ -1,9 +1,6 @@ -/* eslint-disable import/prefer-default-export */ -/* eslint-disable lines-between-class-members */ import { Locator, Page } from '@playwright/test'; -// Glossary page -export class GlossaryPage { +export default class GlossaryPage { readonly page: Page; readonly pageSearchBox: Locator; readonly pageSearchResults: Locator; @@ -18,7 +15,7 @@ export class GlossaryPage { readonly voluntaryRepaymentSectionText: Locator; readonly educationStatisticsMethodology: Locator; - constructor(page) { + constructor(page: Page) { this.page = page; // Locators this.pageSearchBox = page.locator('input#pageSearchForm-input'); @@ -40,6 +37,7 @@ export class GlossaryPage { '//a[text()="Education statistics: methodology"]', ); } + // standard text readonly voluntaryRepaymentSectionParagraphText = 'A borrower can at any time choose to repay some or all of their loan balance early, in addition to any repayments they are liable to make based on their income'; diff --git a/tests/playwright-tests/general-public/pages/basepage.ts b/tests/playwright-tests/general-public/pages/HomePage.ts similarity index 56% rename from tests/playwright-tests/general-public/pages/basepage.ts rename to tests/playwright-tests/general-public/pages/HomePage.ts index 044cf681e06..bf8e617d519 100644 --- a/tests/playwright-tests/general-public/pages/basepage.ts +++ b/tests/playwright-tests/general-public/pages/HomePage.ts @@ -1,21 +1,18 @@ -/* eslint-disable import/prefer-default-export */ -/* eslint-disable lines-between-class-members */ import { Locator, Page } from '@playwright/test'; -import { environment } from '../../utils/env'; +import environment from '../../utils/env'; -// Home page -export class HomePage { +export default class HomePage { readonly page: Page; readonly glossary: Locator; - constructor(page) { + constructor(page: Page) { this.page = page; // Locators this.glossary = page.locator('//h3/a[text()="Glossary"]'); } async navigateToGlossaryPage() { - await this.page.goto(environment.BASE_URL!); + await this.page.goto(environment.PUBLIC_URL); await this.glossary.click(); await this.page.waitForURL('**/glossary'); } diff --git a/tests/playwright-tests/tests/glossary.spec.ts b/tests/playwright-tests/tests/glossary.spec.ts index 7c8522bd2b2..e30538ce83f 100644 --- a/tests/playwright-tests/tests/glossary.spec.ts +++ b/tests/playwright-tests/tests/glossary.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from '@playwright/test'; -import { HomePage } from '../general-public/pages/Basepage'; -import { GlossaryPage } from '../general-public/pages/glossary-page'; +import HomePage from '../general-public/pages/HomePage'; +import GlossaryPage from '../general-public/pages/GlossaryPage'; test.describe('Verify the end to end functionality of glossary page', () => { test.beforeEach(async ({ page }) => { diff --git a/tests/playwright-tests/tests/themes.spec.ts b/tests/playwright-tests/tests/themes.spec.ts new file mode 100644 index 00000000000..6f2edcdf284 --- /dev/null +++ b/tests/playwright-tests/tests/themes.spec.ts @@ -0,0 +1,36 @@ +import { test } from '@playwright/test'; +import environment from '../utils/env'; +import AzureLoginPage from '../admin/azpage/AzureLoginPage'; +import AdminPage from '../admin/pages/AdminPage'; +import ThemesPage from '../admin/pages/ThemesPage'; +import CreateThemePage from '../admin/pages/CreateThemePage'; +import uiTestString from '../utils/uiTestString'; + +test.describe('Verify the end to end functionality of themes and topics', () => { + let adminPage: AdminPage; + let themesPage: ThemesPage; + let createThemePage: CreateThemePage; + let azPage: AzureLoginPage; + + test.beforeEach(async ({ page }) => { + await page.goto(environment.ADMIN_URL); + adminPage = new AdminPage(page); + themesPage = new ThemesPage(page); + createThemePage = new CreateThemePage(page); + azPage = new AzureLoginPage(page); + + await adminPage.clickSignIn(); + await azPage.doSignIn(); + }); + + test('Verify that themes are being created and displayed in the themes home screen', async () => { + await adminPage.manageThemesTopicLink.click(); + await themesPage.createThemeLink.click(); + + const uiThemeTitle = uiTestString('Test theme'); + const uiThemeSummary = uiTestString('Test summary'); + + await createThemePage.doCreateTheme(uiThemeTitle, uiThemeSummary); + await themesPage.checkThemeIsDisplayed(uiThemeTitle); + }); +}); diff --git a/tests/playwright-tests/utils/env.ts b/tests/playwright-tests/utils/env.ts index 0d3e41244f4..3159a163253 100644 --- a/tests/playwright-tests/utils/env.ts +++ b/tests/playwright-tests/utils/env.ts @@ -1,7 +1,14 @@ import dotenv from 'dotenv'; +import generateRunIdentifier from './generateRunIdentifier'; dotenv.config(); -// eslint-disable-next-line import/prefer-default-export -export class environment { - public static BASE_URL = process.env.PUBLIC_URL; -} + +const environment = { + PUBLIC_URL: process.env.PUBLIC_URL ?? '', + ADMIN_URL: process.env.ADMIN_URL ?? '', + ADMIN_EMAIL: process.env.ADMIN_EMAIL ?? '', + ADMIN_PASSWORD: process.env.ADMIN_PASSWORD ?? '', + RUN_IDENTIFIER: generateRunIdentifier(), +}; + +export default environment; diff --git a/tests/playwright-tests/utils/generateRunIdentifier.ts b/tests/playwright-tests/utils/generateRunIdentifier.ts new file mode 100644 index 00000000000..db2a5376d8f --- /dev/null +++ b/tests/playwright-tests/utils/generateRunIdentifier.ts @@ -0,0 +1,11 @@ +export default function generateRunIdentifier() { + const date = new Date(); + const utcFormat = date.toISOString().replace('T', ' ').substring(0, 19); + let random = ''; + + while (random === '') { + random = Math.random().toString(36).slice(2, 7); + } + + return `${utcFormat} ${random}`; +} diff --git a/tests/playwright-tests/utils/uiTestString.ts b/tests/playwright-tests/utils/uiTestString.ts new file mode 100644 index 00000000000..477d07b45cb --- /dev/null +++ b/tests/playwright-tests/utils/uiTestString.ts @@ -0,0 +1,5 @@ +import environment from './env'; + +export default function uiTestString(str: string): string { + return `UI Test - ${str} - ${environment.RUN_IDENTIFIER}`; +}