diff --git a/.eslintrc.js b/.eslintrc.js index 8ecb1541d9..fa65d5cef9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -44,6 +44,8 @@ module.exports = Object.assign({}, sharedConfigs, { '@typescript-eslint/ban-types': 'off', '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/no-this-alias': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': 'off', }), plugins: [...(sharedConfigs.plugins ?? []), '@typescript-eslint'], diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ac7228753b..1827cdcd95 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -90,5 +90,5 @@ jobs: - name: Server Logs if: ${{ failure() }} - run: docker compose logs superdesk + run: docker compose logs server working-directory: e2e/server diff --git a/e2e/client/playwright/authoring.picture.spec.ts b/e2e/client/playwright/authoring.picture.spec.ts new file mode 100644 index 0000000000..6d9abcf46a --- /dev/null +++ b/e2e/client/playwright/authoring.picture.spec.ts @@ -0,0 +1,51 @@ +import {test, expect} from '@playwright/test'; +import {Monitoring} from './page-object-models/monitoring'; +import {restoreDatabaseSnapshot} from './utils'; +import {MediaEditor} from './page-object-models/media-editor'; +import {PictureAuthoring} from './page-object-models/authoring'; +import {MediaUpload} from './page-object-models/upload'; + +test.setTimeout(30000); + +/** + * upload a picture + * edit metadata + * test metadata changes from modal are visible in the editor + */ +test('media metadata editor', async ({page}) => { + await restoreDatabaseSnapshot(); + + const upload = new MediaUpload(page); + const monitoring = new Monitoring(page); + const mediaEditor = new MediaEditor(page); + const pictureAuthoring = new PictureAuthoring(page); + + await page.goto('/#/workspace/monitoring'); + + await monitoring.selectDeskOrWorkspace('Sports'); + await monitoring.openMediaUploadView(); + + await upload.selectFile('iptc-photo.jpg'); + + await expect(mediaEditor.field('field--headline')).toContainText('The Headline'); + + await mediaEditor.field('field--headline').clear(); + await mediaEditor.field('field--headline').fill('picture'); + + await upload.startUpload(); + + await monitoring.executeActionOnMonitoringItem(monitoring.getArticleLocator('picture'), 'Edit'); + + await pictureAuthoring.openMetadataEditor(); + + await mediaEditor.field('field--description_text').fill('test description'); + await mediaEditor.saveMetadata(); + + await expect(pictureAuthoring.field('field--description_text')).toContainText('test description'); + + await pictureAuthoring.field('field--description_text').fill('new description'); + + await pictureAuthoring.openMetadataEditor(); + + await expect(mediaEditor.field('field--description_text')).toContainText('new description'); +}); diff --git a/e2e/client/playwright/page-object-models/authoring.ts b/e2e/client/playwright/page-object-models/authoring.ts index 7c2c865094..970afdb649 100644 --- a/e2e/client/playwright/page-object-models/authoring.ts +++ b/e2e/client/playwright/page-object-models/authoring.ts @@ -1,8 +1,8 @@ -import {Page} from '@playwright/test'; +import {Locator, Page} from '@playwright/test'; import {s} from '../utils'; export class Authoring { - private page: Page; + protected page: Page; constructor(page: Page) { this.page = page; @@ -21,4 +21,15 @@ export class Authoring { .getByRole('button', {name: actionPath[actionPath.length - 1]}) .click(); } + + field(field: string): Locator { + return this.page.locator(s('authoring', field)).getByRole('textbox'); + } } + +export class PictureAuthoring extends Authoring { + async openMetadataEditor(): Promise { + await this.page.locator(s('authoring-field=media', 'image-overlay')).hover(); + await this.page.locator(s('authoring-field=media', 'edit-metadata')).click(); + } +} \ No newline at end of file diff --git a/e2e/client/playwright/page-object-models/media-editor.ts b/e2e/client/playwright/page-object-models/media-editor.ts new file mode 100644 index 0000000000..39880606d0 --- /dev/null +++ b/e2e/client/playwright/page-object-models/media-editor.ts @@ -0,0 +1,19 @@ +import {Locator, Page} from '@playwright/test'; +import {s} from '../utils'; + +export class MediaEditor { + private page: Page; + + constructor(page: Page) { + this.page = page; + } + + field(field: string): Locator { + return this.page.locator(s('media-metadata-editor', field)).getByRole('textbox'); + } + + async saveMetadata(): Promise { + await this.page.locator(s('media-editor', 'apply-metadata-button')).click(); + await this.page.locator(s('change-image', 'done')).click(); + } +} diff --git a/e2e/client/playwright/page-object-models/monitoring.ts b/e2e/client/playwright/page-object-models/monitoring.ts index b1d979fdde..0c2b17e7b1 100644 --- a/e2e/client/playwright/page-object-models/monitoring.ts +++ b/e2e/client/playwright/page-object-models/monitoring.ts @@ -60,4 +60,13 @@ export class Monitoring { } } } + + async openMediaUploadView(): Promise { + await this.page.locator(s('content-create')).click(); + await this.page.locator(s('content-create-dropdown')).getByRole('button', {name: 'Upload media'}).click(); + } + + getArticleLocator(headline: string): Locator { + return this.page.locator(s('article-item=' + headline)); + } } diff --git a/e2e/client/playwright/page-object-models/upload.ts b/e2e/client/playwright/page-object-models/upload.ts new file mode 100644 index 0000000000..8893ec02dd --- /dev/null +++ b/e2e/client/playwright/page-object-models/upload.ts @@ -0,0 +1,21 @@ +import {Page} from '@playwright/test'; +import {s} from '../utils'; +import path from 'path'; + +export class MediaUpload { + private page: Page; + + constructor(page: Page) { + this.page = page; + } + + async selectFile(filename: string): Promise { + await this.page.locator(s('file-upload', 'select-file-button')).click(); + await this.page.locator(s('file-upload', 'image-upload-input')) + .setInputFiles(path.join('test-files', filename)); + } + + async startUpload(): Promise { + await this.page.locator(s('file-upload', 'multi-image-edit--start-upload')).click(); + } +} diff --git a/e2e/client/specs/authoring_spec.ts b/e2e/client/specs/authoring_spec.ts index 41e7b25176..c4640c52b8 100644 --- a/e2e/client/specs/authoring_spec.ts +++ b/e2e/client/specs/authoring_spec.ts @@ -29,14 +29,7 @@ function uploadMedia(imagePathAbsolute) { el(['media-metadata-editor', 'field--headline'], by.tagName('[contenteditable]')) .sendKeys('image headline'); el(['media-metadata-editor', 'field--slugline'], by.tagName('[contenteditable]')) - .sendKeys('image headline'); - el(['media-metadata-editor', 'field--alt_text'], by.tagName('[contenteditable]')) - .sendKeys('image alt text'); - - selectFromMetaTermsDropdown('anpa_category', ['Finance']); - - selectFromMetaTermsDropdown('subject', ['arts, culture and entertainment', 'archaeology']); - + .sendKeys('image slugline'); el(['media-metadata-editor', 'field--description_text'], by.tagName('[contenteditable]')) .sendKeys('image description'); diff --git a/e2e/client/test-files/iptc-photo.jpg b/e2e/client/test-files/iptc-photo.jpg new file mode 100644 index 0000000000..0fed9af53f Binary files /dev/null and b/e2e/client/test-files/iptc-photo.jpg differ diff --git a/e2e/server/docker-compose.yml b/e2e/server/docker-compose.yml index 1637326bf8..85de01bd13 100644 --- a/e2e/server/docker-compose.yml +++ b/e2e/server/docker-compose.yml @@ -1,3 +1,4 @@ +name: superdesk-client-core_e2e version: "3.2" services: redis: @@ -22,7 +23,7 @@ services: command: --replSet rs0 elastic: - image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2 + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.22 ports: - "9200:9200" networks: @@ -33,7 +34,7 @@ services: tmpfs: - /usr/share/elasticsearch/data - superdesk: + server: build: . ports: - "5000:5000" diff --git a/e2e/server/settings.py b/e2e/server/settings.py index 553800cf8c..59a7776843 100644 --- a/e2e/server/settings.py +++ b/e2e/server/settings.py @@ -27,3 +27,21 @@ LEGAL_ARCHIVE = True DEFAULT_TIMEZONE = "Europe/London" + +VALIDATOR_MEDIA_METADATA = { + "slugline": { + "required": False, + }, + "headline": { + "required": False, + }, + "description_text": { + "required": True, + }, + "byline": { + "required": False, + }, + "copyrightnotice": { + "required": False, + }, +} diff --git a/package-lock.json b/package-lock.json index a53570dd90..de5bb468b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1319,7 +1319,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "prop-types": { "version": "15.8.1", diff --git a/scripts/apps/archive/views/upload.html b/scripts/apps/archive/views/upload.html index 42922d8a31..50243aeb11 100644 --- a/scripts/apps/archive/views/upload.html +++ b/scripts/apps/archive/views/upload.html @@ -9,8 +9,9 @@ get-icon-for-item-type="getIconForItemType" get-progress="getProgress" on-remove-item="onRemoveItem" - upload-in-progress="saving"> - + upload-in-progress="saving", + data-test-id="file-upload" +> diff --git a/scripts/apps/authoring/authoring/directives/ArticleEditDirective.ts b/scripts/apps/authoring/authoring/directives/ArticleEditDirective.ts index b2f1f8c5a5..68dcf053b0 100644 --- a/scripts/apps/authoring/authoring/directives/ArticleEditDirective.ts +++ b/scripts/apps/authoring/authoring/directives/ArticleEditDirective.ts @@ -37,6 +37,7 @@ interface IScope extends ng.IScope { extra: any; refreshTrigger: number; autosave(item: any): any; + generateHtml(): void; modifySignOff(item: any): void; updateDateline(item: any, city: any): void; resetNumberOfDays(dateline: any, datelineMonth?: any): void; @@ -342,6 +343,10 @@ export function ArticleEditDirective( * @description Opens the Change Image Controller to modify the image metadata. */ scope.editMedia = (defaultTab = 'view') => { + // generate html before opening the modal to make + // any changes done in the authoring visible there + scope.generateHtml(); + let showTabs = []; scope.mediaLoading = true; @@ -388,7 +393,7 @@ export function ArticleEditDirective( scope.articleEdit.$setDirty(); } } else { - scope.save(); + scope.save({generateHtml: false}); } }) .finally(() => { diff --git a/scripts/apps/authoring/authoring/directives/AuthoringDirective.ts b/scripts/apps/authoring/authoring/directives/AuthoringDirective.ts index c08c10103d..ae23d69845 100644 --- a/scripts/apps/authoring/authoring/directives/AuthoringDirective.ts +++ b/scripts/apps/authoring/authoring/directives/AuthoringDirective.ts @@ -95,12 +95,9 @@ export function AuthoringDirective( $scope.tabsPinned = false; var _closing; - var mediaFields = {}; var userDesks; const UNIQUE_NAME_ERROR = gettext('Error: Unique Name is not unique.'); - const MEDIA_TYPES = ['video', 'picture', 'audio']; - const isPersonalSpace = $location.path() === '/workspace/personal'; $scope.eventListenersToRemoveOnUnmount = []; $scope.toDeskEnabled = false; // Send an Item to a desk @@ -187,7 +184,7 @@ export function AuthoringDirective( function getCurrentTemplate() { const item: IArticle | null = $scope.item; - if (item.type === 'composite') { + if (item.type !== 'text') { $scope.currentTemplate = {}; } else { if (typeof item?.template !== 'string') { @@ -270,11 +267,11 @@ export function AuthoringDirective( /** * Create a new version */ - $scope.save = function() { + $scope.save = function({generateHtml = true} = {}) { return authoring.save( $scope.origItem, $scope.item, - $scope.requestEditor3DirectivesToGenerateHtml, + generateHtml ? $scope.requestEditor3DirectivesToGenerateHtml : [], ).then((res) => { $scope.dirty = false; _.merge($scope.item, res); @@ -557,9 +554,7 @@ export function AuthoringDirective( // delay required for loading state to render // before possibly long operation (with huge articles) setTimeout(() => { - for (const fn of $scope.requestEditor3DirectivesToGenerateHtml) { - fn(); - } + $scope.generateHtml(); resolve(); }); @@ -676,9 +671,7 @@ export function AuthoringDirective( _closing = true; // Request to generate html before we pass scope variables - for (const fn of ($scope.requestEditor3DirectivesToGenerateHtml ?? [])) { - fn(); - } + $scope.generateHtml(); // returned promise used by superdesk-fi return authoringApiCommon.closeAuthoringStep2($scope, $rootScope); @@ -859,7 +852,7 @@ export function AuthoringDirective( $scope.firstLineConfig.wordCount = $scope.firstLineConfig.wordCount ?? true; const _autosave = debounce((timeout) => { - $scope.requestEditor3DirectivesToGenerateHtml.forEach((fn) => fn()); + $scope.generateHtml(); return authoring.autosave( $scope.item, @@ -882,6 +875,10 @@ export function AuthoringDirective( _autosave(timeout); }; + $scope.generateHtml = () => { + $scope.requestEditor3DirectivesToGenerateHtml.forEach((fn) => fn()); + }; + $scope.sendToNextStage = function() { sdApi.article.sendItemToNextStage($scope.item).then(() => { $scope.$applyAsync(); diff --git a/scripts/apps/authoring/views/article-edit.html b/scripts/apps/authoring/views/article-edit.html index 9fa91f352f..7b6dbc681f 100644 --- a/scripts/apps/authoring/views/article-edit.html +++ b/scripts/apps/authoring/views/article-edit.html @@ -279,18 +279,20 @@ sd-validation-error="error.media" data-required="schema.media.required" tabindex="{{editor.media.order}}" - sd-width="{{editor.media.sdWidth|| 'full'}}"> - + sd-width="{{editor.media.sdWidth|| 'full'}}" + data-test-id="authoring-field" + data-test-value="media" +>
-
-
- - - +
+
+ + +
@@ -371,6 +373,7 @@ data-required="schema.description_text.required" data-validation-error="error.description_text" data-validate-characters="schema.description_text.validate_characters" + data-test-id="field--description_text" >
diff --git a/scripts/apps/authoring/views/change-image.html b/scripts/apps/authoring/views/change-image.html index 75357398ae..dcf6484569 100644 --- a/scripts/apps/authoring/views/change-image.html +++ b/scripts/apps/authoring/views/change-image.html @@ -19,7 +19,7 @@
-