Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
32a0512
refactor(content-drive): migrate DotFolderListView to @dotcms/ui
nicobytes Aug 5, 2026
4302e08
feat(data-access): enhance folder tree utilities and add tests
nicobytes Aug 7, 2026
5e49ebf
Merge branch 'main' into issue-36702-asset-picker
nicobytes Aug 7, 2026
c95a372
Refactor content-drive chip filters into reusable @dotcms/ui components
nicobytes Aug 7, 2026
7a59dab
Fix Nx build ordering so dotcms-ui waits on dotcms-webcomponents
nicobytes Aug 7, 2026
f3d23b0
Merge branch 'main' into issue-36702-asset-picker
nicobytes Aug 7, 2026
5e08a7c
Add headless AssetPicker browse store with browse/folder-tree/selection
nicobytes Aug 7, 2026
d4ec739
Reformat import statement to fit project line-length style
nicobytes Aug 7, 2026
b4de567
Merge branch 'issue-36702-asset-picker' of github.com:dotCMS/core int…
nicobytes Aug 7, 2026
4f0d069
Merge branch 'main' into issue-36702-asset-picker
nicobytes Aug 7, 2026
40e3d77
Extract shared upload UI into a reusable AssetPicker kit in @dotcms/ui
nicobytes Aug 7, 2026
3c179c7
Merge branch 'main' into issue-36702-asset-picker
nicobytes Aug 7, 2026
94f7fa5
Extract shared upload UI into a reusable AssetPicker kit in @dotcms/ui
nicobytes Aug 7, 2026
fe7ce7e
Replace legacy browser selector with AssetPicker in file field
nicobytes Aug 7, 2026
7e7546d
Merge branch 'main' into issue-36702-asset-picker
nicobytes Aug 7, 2026
32c9934
Restrict asset-picker content-type selector to asset base types, fix fol
nicobytes Aug 7, 2026
bf31fea
Fix import ordering and add missing state type constraint to withAssetSe
nicobytes Aug 7, 2026
507de73
Merge branch 'issue-36702-asset-picker' of github.com:dotCMS/core int…
nicobytes Aug 8, 2026
3986990
Merge branch 'main' into issue-36702-asset-picker
nicobytes Aug 8, 2026
011f727
Merge branch 'main' into issue-36702-asset-picker
nicobytes Aug 10, 2026
cf81ada
Refactor AssetPicker into a multi-site browser with full-screen support
nicobytes Aug 10, 2026
9a63b03
Add asset-picker header, sidebar, and full-screen dialog building blocks
nicobytes Aug 10, 2026
1bde8cb
Adjust asset picker dialog dimensions
nicobytes Aug 10, 2026
c10a584
Rework asset-picker folder tree to update by node key, not reference
nicobytes Aug 10, 2026
691eeb9
Merge branch 'main' into issue-36702-asset-picker
nicobytes Aug 10, 2026
6c68e2c
Increase asset picker dialog max dimensions for large monitors
nicobytes Aug 10, 2026
548e8be
Merge branch 'issue-36702-asset-picker' of github.com:dotCMS/core int…
nicobytes Aug 10, 2026
7487de5
Fix asset picker base-type filtering to enforce allowedBaseTypes as a bo
nicobytes Aug 10, 2026
5ccfc27
Add E2E coverage and test hooks for AssetPicker selection flow
nicobytes Aug 11, 2026
adc25e7
Merge branch 'main' into issue-36702-asset-picker
nicobytes Aug 11, 2026
1e39da9
Merge branch 'main' into issue-36702-asset-picker
nicobytes Aug 11, 2026
7badce4
Fix asset picker regressions: dual search boxes, tree highlight, confirm
nicobytes Aug 11, 2026
d27c15b
Merge branch 'issue-36702-asset-picker' of github.com:dotCMS/core int…
nicobytes Aug 11, 2026
f54976c
Replace GlobalStore site lookup with DotSiteService in file field compon
nicobytes Aug 11, 2026
144a30e
Merge branch 'main' into issue-36702-asset-picker
nicobytes Aug 11, 2026
5fb6e75
Merge branch 'main' into issue-36702-asset-picker
nicobytes Aug 11, 2026
ef7f3f4
Fix Playwright E2E CI flakiness from shared-memory and heap exhaustion
nicobytes Aug 11, 2026
3853c25
Update playwright config comment to explain why worker count shouldn't b
nicobytes Aug 11, 2026
c49471b
Merge branch 'issue-36702-asset-picker' of github.com:dotCMS/core int…
nicobytes Aug 11, 2026
26ea4a2
Fix picker asset preview test to use a PNG instead of text file
nicobytes Aug 11, 2026
5624a08
Merge remote-tracking branch 'origin/main' into issue-36702-asset-picker
Copilot Aug 14, 2026
1e2639f
Refactor asset picker and image editor onto shared dialog shell with spl
nicobytes Aug 14, 2026
bf8c42e
Add DotDialog shell components for shared dialog chrome
nicobytes Aug 14, 2026
0af6aa0
Fix folder list rows briefly draggable during read-only reorder
nicobytes Aug 14, 2026
a4346a1
Extend asset picker to Story Block image/video/audio and rework dialog c
nicobytes Aug 14, 2026
7e1b2ae
Merge branch 'main' into issue-36702-asset-picker
nicobytes Aug 14, 2026
5d34a31
Extend AssetPicker to Block Editor and WYSIWYG fields, unifying insertio
nicobytes Aug 14, 2026
16514d0
Fix e2e asset-picker test flakiness and workflow API delete bug
nicobytes Aug 14, 2026
3a3bb82
Merge branch 'main' into issue-36702-asset-picker
nicobytes Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions core-web/apps/dotcms-ui-e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,14 @@ export default defineConfig({
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Parallelize CI (2 workers); local keeps Playwright default. */
/*
* Parallelize CI (2 workers); local keeps Playwright default.
*
* Do NOT lower this to work around a crashing shard. The 1 -> 2 bump is a measured improvement
* from #36567 / PR #36647: the Playwright phase went from ~49m to a <30m target, so going back
* roughly doubles E2E time for every PR in the repo. If concurrency ever is proven to be the
* cause, that belongs in its own change against #36567, not smuggled into a feature PR.
*/
workers: process.env.CI ? 2 : undefined,
timeout: 60000,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
Expand All @@ -66,7 +73,16 @@ export default defineConfig({
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
headless: headless
headless: headless,
launchOptions: {
/*
* Chromium puts its shared-memory allocations in /dev/shm, which a container gives 64MB
* of by default. Exhausting it crashes the browser process outright — a SIGSEGV with no
* Playwright output and no JUnit report, which is exactly how the CI shard died. This
* flag moves those allocations to regular temp files instead.
*/
args: ['--disable-dev-shm-usage']
}
},
/* Run your local dev server before starting the tests */
webServer:
Expand Down
13 changes: 13 additions & 0 deletions core-web/apps/dotcms-ui-e2e/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@
<e2e.test.env>local</e2e.test.env>
<e2e.frontend.dir>../../</e2e.frontend.dir>
<e2e.playwright.args></e2e.playwright.args>
<!--
NODE_OPTIONS for the process that runs Playwright, raised from the default because the CI
shard was dying mid-run. Override per invocation while tuning, with
-De2e.node.options=... (note: XML comments cannot contain a double hyphen, so the flag
name is spelled without its leading dashes below).

This lifts V8's old-space ceiling and nothing else. It does NOT address a SIGSEGV or an
OOM-killed container: a JS heap exhaustion aborts with "Reached heap limit" and exit 134,
so for any other crash signature look at the memory the container has, not at this value.
max-old-space-size is the flag being set.
-->
<e2e.node.options>--max-old-space-size=4096</e2e.node.options>
<e2e.test.cmd>nx run dotcms-ui-e2e:e2e --configuration=${e2e.test.env} -- ${e2e.playwright.args}</e2e.test.cmd>
<e2e.post.test.cmd>exec sh -c "mkdir -p apps/dotcms-ui-e2e/target/playwright-reports &amp;&amp; cp apps/dotcms-ui-e2e/test-results/junit.xml apps/dotcms-ui-e2e/target/playwright-reports/junit.xml"</e2e.post.test.cmd>
<tomcat.port>8080</tomcat.port>
Expand Down Expand Up @@ -90,6 +102,7 @@
<environmentVariables>
<CI>true</CI>
<CURRENT_ENV>${e2e.test.env}</CURRENT_ENV>
<NODE_OPTIONS>${e2e.node.options}</NODE_OPTIONS>
<PATH>${node.install.dir}:${env.PATH}</PATH>
</environmentVariables>
</configuration>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { type Locator, type Page, expect } from '@playwright/test';

/**
* Locator helper for the AssetPicker dialog — the one "browse for an existing asset" modal in the
* product. Four entry points open it, which is why this lives in `components/` rather than under any
* one field's `helpers/`:
*
* - the File field's "Select Existing File"
* - the Image field's "Select Existing Image"
* - the Story Block's insert image / video / audio toolbar buttons and slash commands
* - the WYSIWYG (TinyMCE) field's insert-image button
*
* The picker renders its own header (the dialog is opened with `showHeader: false`), so everything
* here is scoped to the picker root rather than to PrimeNG's chrome.
*/
export class AssetPickerDialog {
readonly root: Locator;
readonly title: Locator;
readonly closeButton: Locator;
readonly fullscreenButton: Locator;
readonly search: Locator;
readonly sidebar: Locator;
readonly treeSearch: Locator;
readonly list: Locator;
readonly rows: Locator;
readonly cancelButton: Locator;
readonly confirmButton: Locator;

constructor(private page: Page) {
this.root = page.getByTestId('asset-picker');
// Title and close come from the shared dialog shell, so their ids are not picker-specific.
this.title = this.root.getByTestId('dialog-title');
this.closeButton = this.root.getByTestId('dialog-close-btn');
this.fullscreenButton = this.root.getByTestId('asset-picker-fullscreen-btn');
// Two search boxes are on screen at once, so each carries its own id — a shared one made
// every selector here ambiguous and was what broke this suite in CI.
this.search = this.root.getByTestId('asset-picker-search-input');
this.sidebar = this.root.getByTestId('asset-picker-sidebar');
this.treeSearch = this.root.getByTestId('asset-picker-tree-search-input');
this.list = this.root.getByTestId('asset-picker-list');
this.rows = this.list.getByTestId('item-row');
this.cancelButton = this.root.getByTestId('asset-picker-cancel');
this.confirmButton = this.root.getByTestId('asset-picker-confirm');
}

async waitForVisible(): Promise<void> {
await expect(this.root).toBeVisible({ timeout: 15000 });
}

async expectClosed(): Promise<void> {
await expect(this.root).toBeHidden({ timeout: 10000 });
}

async expectTitle(text: string): Promise<void> {
await expect(this.title).toHaveText(text);
}

/**
* Types a term into the asset search and waits for the results it produces.
*
* The search is debounced and widens the scope to the whole site, which is what makes it a
* reliable way to reach a seeded asset without depending on which folder the picker opened on.
*/
async searchFor(term: string): Promise<void> {
const response = this.page.waitForResponse(
(res) => res.url().includes('/api/v1/drive/search') && res.status() === 200,
{ timeout: 30000 }
);
await this.search.fill(term);
await response;
}

/** The row whose title cell contains `name`. */
row(name: string): Locator {
return this.rows.filter({ hasText: name });
}

async expectRowVisible(name: string): Promise<void> {
await expect(this.row(name)).toBeVisible({ timeout: 15000 });
}

/**
* Selects a row by clicking its title — the content, not the cell padding.
*
* Clicking the title specifically is the point: in the picker the whole row selects, whereas in
* Content Drive the title opens the item instead.
*/
async selectRowByTitle(name: string): Promise<void> {
await this.row(name).getByTestId('item-title-text').click();
}

async expectRowSelected(name: string): Promise<void> {
await expect(this.row(name).getByRole('radio')).toBeChecked();
}

async expectConfirmEnabled(): Promise<void> {
await expect(this.confirmButton.getByRole('button')).toBeEnabled();
}

async expectConfirmDisabled(): Promise<void> {
await expect(this.confirmButton.getByRole('button')).toBeDisabled();
}

async confirm(): Promise<void> {
await this.confirmButton.getByRole('button').click();
}

async cancel(): Promise<void> {
await this.cancelButton.getByRole('button').click();
}

async close(): Promise<void> {
await this.closeButton.getByRole('button').click();
}

/** Rows offer no per-row actions here — a row exists to be picked, not managed. */
async expectNoRowActions(): Promise<void> {
await expect(this.list.getByTestId('kebab-menu-button')).toHaveCount(0);
}
}
49 changes: 48 additions & 1 deletion core-web/apps/dotcms-ui-e2e/src/requests/contentlets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,48 @@ export async function createContentlet(
return entity as Contentlet;
}

/**
* Creates a dotAsset contentlet from an in-memory file, in one multipart call.
*
* Mirrors what the product itself does (`DotUploadFileService.uploadDotAsset` →
* `DotWorkflowActionsFireService.newContentlet`): a `PUT .../fire/NEW` whose body carries the binary
* as the `file` part and the contentlet as a `json` part. Going through `/api/v1/temp` first would
* work too, but that endpoint fingerprints the caller (session + origin), so a single call is one
* less thing to get wrong from a test runner.
*
* `indexPolicy=WAIT_FOR` is what makes this usable for seeding — the asset is searchable by the time
* the request returns, so a test can open a picker and expect to find it.
*
* @param request - Playwright APIRequestContext
* @param file - The file to store, as `{ name, mimeType, buffer }`
* @param hostFolder - Site identifier or folder id the asset is created under
* @returns The created contentlet
*/
export async function createDotAsset(
request: APIRequestContext,
file: { name: string; mimeType: string; buffer: Buffer },
hostFolder: string
): Promise<Contentlet> {
const endpoint = `/api/v1/workflow/actions/default/fire/NEW?indexPolicy=WAIT_FOR`;
const response = await request.put(endpoint, {
multipart: {
file: { name: file.name, mimeType: file.mimeType, buffer: file.buffer },
json: JSON.stringify({
contentlet: { contentType: 'dotAsset', file: file.name, hostFolder }
})
},
headers: {
Authorization: generateBase64Credentials(admin1.username, admin1.password)
}
});

expect(response.status()).toBe(200);

const responseData = await response.json();

return responseData.entity as Contentlet;
}

/**
* Relates content via the relationship API.
* Uses the PUBLISH workflow action to save content with relationship data.
Expand Down Expand Up @@ -79,6 +121,11 @@ export async function relateContent(
/**
* Deletes contentlets by their identifiers.
*
* Fires DESTROY through the WORKFLOW resource. `/api/v1/content/actions/...` — which this used to
* call — does not exist and answered 404 for every contentlet, so nothing was ever deleted and the
* 404 was swallowed as "already gone". Every suite using this leaked its seeded content into the
* environment on each run.
*
* @param request - Playwright APIRequestContext
* @param identifiers - Array of contentlet identifiers to delete
*/
Expand All @@ -87,7 +134,7 @@ export async function deleteContentlets(
identifiers: string[]
): Promise<void> {
for (const identifier of identifiers) {
const endpoint = `/api/v1/content/actions/default/fire/DESTROY?identifier=${identifier}`;
const endpoint = `/api/v1/workflow/actions/default/fire/DESTROY?identifier=${identifier}`;
const response = await request.put(endpoint, {
headers: {
Authorization: generateBase64Credentials(admin1.username, admin1.password)
Expand Down
Loading
Loading