Skip to content
Snippets Groups Projects
Commit 8881c176 authored by Narendra Singh Rathore's avatar Narendra Singh Rathore
Browse files

Merge branch '3489054-add-to-cart-threshold' into '2.0.x'

Resolve #3489054 "Add to cart threshold"

See merge request !644
parents f5bbcf1c e07bbee9
No related branches found
No related tags found
No related merge requests found
Pipeline #388067 failed
Showing with 337 additions and 273 deletions
......@@ -34,6 +34,7 @@ class BrowserController extends ControllerBase {
return [
'#type' => 'project_browser',
'#source' => $source,
'#max_selections' => 1,
];
}
......
......@@ -97,7 +97,8 @@ final class ProjectBrowser implements ElementInterface, ContainerFactoryPluginIn
*/
public function attachProjectBrowserSettings(array $element): array {
$element['#attached']['drupalSettings']['project_browser'] = $this->getDrupalSettings(
$element['#source']
$element['#source'],
$element['#max_selections'] ?? NULL
);
return $element;
}
......@@ -107,11 +108,16 @@ final class ProjectBrowser implements ElementInterface, ContainerFactoryPluginIn
*
* @param string $source
* The ID of the source plugin to query for projects.
* @param int|null $max_selections
* The maximum number of project to install at once, or NULL for no limit.
*
* @return array
* An array of Drupal settings.
*/
private function getDrupalSettings(string $source): array {
private function getDrupalSettings(string $source, ?int $max_selections = 1): array {
if (is_int($max_selections) && $max_selections <= 0) {
throw new \InvalidArgumentException('$max_selections must be a positive integer or NULL.');
}
$current_sources = [
$source => $this->enabledSourceHandler->getCurrentSources()[$source],
];
......@@ -149,6 +155,7 @@ final class ProjectBrowser implements ElementInterface, ContainerFactoryPluginIn
fn (ProjectBrowserSourceInterface $source) => $source->getFilterDefinitions(),
$current_sources,
),
'max_selections' => $max_selections,
];
}
......
......@@ -59,6 +59,9 @@
.project__action_button:hover {
cursor: pointer;
}
.project__action_button:disabled {
cursor: not-allowed;
}
.project__action_button--fixed {
margin-inline-start: auto;
position: fixed;
......
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
<script>
import { get } from 'svelte/store';
import { openPopup } from './popup';
import { BASE_URL } from './constants';
import { queueList, updated, activeTab, clearQueueForTab } from './stores';
import { queueList, activeTab, updated } from './stores';
import { processQueue } from './QueueProcessor';
import Loading from './Loading.svelte';
import LoadingEllipsis from './Project/LoadingEllipsis.svelte';
......@@ -10,205 +9,21 @@
const { Drupal } = window;
const currentQueueList = get(queueList)[$activeTab] || [];
const currentQueueList = get(queueList)[get(activeTab)] || [];
const queueLength = Object.keys(currentQueueList).length;
const projectsToActivate = [];
const projectsToDownloadAndActivate = [];
const handleError = async (errorResponse) => {
// If an error occurred, set loading to false so the UI no longer reports
// the download/install as in progress.
loading = false;
// The error can take on many shapes, so it should be normalized.
let err = '';
if (typeof errorResponse === 'string') {
err = errorResponse;
} else {
err = await errorResponse.text();
}
try {
// See if the error string can be parsed as JSON. If not, the block
// is exited before the `err` string is overwritten.
const parsed = JSON.parse(err);
err = parsed;
} catch (error) {
// The catch behavior is established before the try block.
}
const errorMessage = err.message || err;
// The popup function expects an element, so a div containing the error
// message is created here for it to display in a modal.
const div = document.createElement('div');
const currentUrl =
window.location.pathname + window.location.search + window.location.hash;
if (err.unlock_url) {
try {
const unlockUrl = new URL(err.unlock_url, BASE_URL);
unlockUrl.searchParams.set('destination', currentUrl);
const updatedMessage = errorMessage.replace(
'[+ unlock link]',
`<a href="${
unlockUrl.pathname + unlockUrl.search
}" id="unlock-link">${Drupal.t('unlock link')}</a>`,
);
div.innerHTML += `<p>${updatedMessage}</p>`;
} catch (urlError) {
div.innerHTML += `<p>${errorMessage}</p>`;
}
} else {
div.innerHTML += `<p>${errorMessage}</p>`;
}
openPopup(div, {
title: 'Error while installing package(s)',
});
};
/**
* Actives already-downloaded projects.
*
* @param {string[]} projectIds
* An array of project IDs to activate.
*
* @return {Promise<void>}
* A promise that resolves when the project is activated.
*/
async function activateProject(projectIds) {
const url = `${BASE_URL}admin/modules/project_browser/activate`;
const installResponse = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(projectIds),
});
if (!installResponse.ok) {
await handleError(installResponse);
loading = false;
return;
}
try {
const responseContent = JSON.parse(await installResponse.text());
if (responseContent.hasOwnProperty('redirect')) {
window.location.href = responseContent.redirect;
}
} catch (err) {
await handleError(installResponse);
}
}
/**
* Performs the requests necessary to download and activate project via Package Manager.
*
* @param {string[]} projectIds
* An array of project IDs to download and activate.
*
* @return {Promise<void>}
* Returns a promise that resolves once the download and activation process is complete.
*/
async function doRequests(projectIds) {
const beginInstallUrl = `${BASE_URL}admin/modules/project_browser/install-begin?source=${$activeTab}`;
const beginInstallResponse = await fetch(beginInstallUrl);
if (!beginInstallResponse.ok) {
await handleError(beginInstallResponse);
} else {
const beginInstallData = await beginInstallResponse.json();
const stageId = beginInstallData.stage_id;
// The process of adding a module is separated into four stages, each
// with their own endpoint. When one stage completes, the next one is
// requested.
const installSteps = [
{
url: `${BASE_URL}admin/modules/project_browser/install-require/${stageId}`,
method: 'POST',
},
{
url: `${BASE_URL}admin/modules/project_browser/install-apply/${stageId}`,
method: 'GET',
},
{
url: `${BASE_URL}admin/modules/project_browser/install-post_apply/${stageId}`,
method: 'GET',
},
{
url: `${BASE_URL}admin/modules/project_browser/install-destroy/${stageId}`,
method: 'GET',
},
];
// eslint-disable-next-line no-restricted-syntax,guard-for-in
for (const step of installSteps) {
const options = {
method: step.method,
};
// Additional options need to be added when the request method is POST.
// This is specifically required for the `install-require` step.
if (step.method === 'POST') {
options.headers = {
'Content-Type': 'application/json',
};
// Set the request body to include the project(s) id as an array.
options.body = JSON.stringify(projectsToDownloadAndActivate);
}
// eslint-disable-next-line no-await-in-loop
const stepResponse = await fetch(step.url, options);
if (!stepResponse.ok) {
// eslint-disable-next-line no-await-in-loop
const errorMessage = await stepResponse.text();
// eslint-disable-next-line no-console
console.warn(
`failed request to ${step.url}: ${errorMessage}`,
stepResponse,
);
// eslint-disable-next-line no-await-in-loop
await handleError(errorMessage);
return;
}
}
await activateProject(projectIds);
}
}
async function processQueue() {
// eslint-disable-next-line no-restricted-syntax,guard-for-in
for (const proj of currentQueueList) {
if (proj.status === 'absent') {
projectsToDownloadAndActivate.push(proj.id);
} else if (proj.status === 'present') {
projectsToActivate.push(proj.id);
}
}
const handleClick = async () => {
loading = true;
document.body.style.pointerEvents = 'none';
if (projectsToActivate.length > 0) {
await activateProject(projectsToActivate);
}
if (projectsToDownloadAndActivate.length > 0) {
await doRequests(projectsToDownloadAndActivate);
}
await processQueue();
loading = false;
document.body.style.pointerEvents = 'auto';
clearQueueForTab($activeTab);
// eslint-disable-next-line no-restricted-syntax,guard-for-in
for (const project of currentQueueList) {
project.status = 'active';
}
$updated = new Date().getTime();
}
};
</script>
<button
class="project__action_button project__action_button--fixed"
on:click={processQueue}
on:click={handleClick}
disabled={loading}
>
{#if loading}
<Loading />
......
<script>
import { PACKAGE_MANAGER } from '../constants';
import { PACKAGE_MANAGER, MAX_SELECTIONS } from '../constants';
import { openPopup, getCommandsPopupMessage } from '../popup';
import ProjectButtonBase from './ProjectButtonBase.svelte';
import ProjectStatusIndicator from './ProjectStatusIndicator.svelte';
......@@ -10,11 +10,25 @@
updated,
activeTab,
} from '../stores';
import LoadingEllipsis from './LoadingEllipsis.svelte';
import { processQueue } from '../QueueProcessor';
// eslint-disable-next-line import/no-mutable-exports,import/prefer-default-export
export let project;
const { Drupal } = window;
const processMultipleProjects = MAX_SELECTIONS === null || MAX_SELECTIONS > 1;
$: isInQueue =
$queueList[$activeTab] &&
$queueList[$activeTab].some((item) => item.id === project.id);
const queueFull =
$queueList[$activeTab] &&
// If MAX_SELECTIONS is null (no limit), then the queue is never full.
Object.keys($queueList[$activeTab]).length === MAX_SELECTIONS;
let loading = false;
function handleAddToQueueClick(singleProject) {
addToQueue($activeTab, singleProject);
......@@ -25,6 +39,22 @@
removeFromQueue($activeTab, projectId);
$updated = new Date().getTime();
}
const onClick = async () => {
if (processMultipleProjects) {
if (isInQueue) {
handleDequeueClick(project.id);
} else {
handleAddToQueueClick(project);
}
} else {
handleAddToQueueClick(project);
loading = true;
await processQueue();
loading = false;
$updated = new Date().getTime();
}
};
</script>
<div class="pb-actions">
......@@ -37,17 +67,12 @@
{:else}
<span>
{#if PACKAGE_MANAGER.available && PACKAGE_MANAGER.errors.length === 0}
{#if ($queueList[$activeTab] && $queueList[$activeTab].some((item) => item.id === project.id)) || false}
<ProjectButtonBase click={() => handleDequeueClick(project.id)}>
{@html Drupal.t(
'Deselect <span class="visually-hidden">@title</span>',
{
'@title': project.title,
},
)}
{#if isInQueue && !processMultipleProjects}
<ProjectButtonBase>
<LoadingEllipsis />
</ProjectButtonBase>
{:else}
<ProjectButtonBase click={() => handleAddToQueueClick(project)}>
{:else if queueFull && !isInQueue && processMultipleProjects}
<ProjectButtonBase disabled>
{@html Drupal.t(
'Select <span class="visually-hidden">@title</span>',
{
......@@ -55,6 +80,31 @@
},
)}
</ProjectButtonBase>
{:else}
<ProjectButtonBase click={onClick}>
{#if isInQueue}
{@html Drupal.t(
'Deselect <span class="visually-hidden">@title</span>',
{
'@title': project.title,
},
)}
{:else if processMultipleProjects}
{@html Drupal.t(
'Select <span class="visually-hidden">@title</span>',
{
'@title': project.title,
},
)}
{:else}
{@html Drupal.t(
'Install <span class="visually-hidden">@title</span>',
{
'@title': project.title,
},
)}
{/if}
</ProjectButtonBase>
{/if}
{:else if project.commands}
{#if project.commands.match(/^https?:\/\//)}
......
......@@ -7,7 +7,6 @@
import { numberFormatter } from './util';
import ProcessQueueButton from './ProcessQueueButton.svelte';
import {
sourceFilters,
filters,
rowsCount,
moduleCategoryFilter,
......@@ -25,16 +24,15 @@
} from './stores';
import MediaQuery from './MediaQuery.svelte';
import {
FILTERS,
ACTIVELY_MAINTAINED_ID,
COVERED_ID,
ALL_VALUES_ID,
DEFAULT_SOURCE_ID,
BASE_URL,
FULL_MODULE_PATH,
SORT_OPTIONS,
ACTIVE_PLUGINS,
PACKAGE_MANAGER,
MAX_SELECTIONS,
} from './constants';
// cspell:ignore tabwise
......@@ -176,6 +174,9 @@
* Load remote data when the Svelte component is mounted.
*/
onMount(async () => {
if (MAX_SELECTIONS === 1) {
$queueList = {};
}
const savedPageSize = localStorage.getItem('pageSize');
if (savedPageSize) {
pageSize.set(Number(savedPageSize));
......@@ -249,36 +250,6 @@
preferredView.set(val);
}
async function toggleRows(event) {
if (event.detail.pluginId === $activeTab) {
return;
}
$categoryCheckedTrack[$activeTab] = $moduleCategoryFilter;
$moduleCategoryFilter = [];
$activeTab = event.detail.pluginId;
if ($activeTab in FILTERS) {
$sourceFilters = FILTERS[$activeTab];
}
$moduleCategoryFilter =
typeof $categoryCheckedTrack[$activeTab] !== 'undefined'
? $categoryCheckedTrack[$activeTab]
: [];
$sortCriteria = SORT_OPTIONS[$activeTab];
const sortMatch = $sortCriteria.find((option) => option.id === $sort);
if (typeof sortMatch === 'undefined') {
$sort = $sortCriteria[0].id;
}
searchComponent.onSearch(event);
const { target } = event.detail.event;
const parent = target.parentNode;
// Remove all current selected tabs
parent
.querySelectorAll('[aria-selected="true"]')
.forEach((t) => t.setAttribute('aria-selected', false));
// Set this tab as selected
target.setAttribute('aria-selected', true);
}
/**
* Refreshes the live region after a filter or search completes.
*/
......@@ -398,7 +369,7 @@
{#each rows as row, index (row)}
<Project {toggleView} project={row} />
{/each}
{#if PACKAGE_MANAGER.available && hasItemsInQueue($activeTab)}
{#if PACKAGE_MANAGER.available && hasItemsInQueue($activeTab) && (MAX_SELECTIONS === null || MAX_SELECTIONS > 1)}
<ProcessQueueButton />
{/if}
{/key}
......
import { get } from 'svelte/store';
import { openPopup } from './popup';
import { BASE_URL } from './constants';
import { queueList, activeTab, clearQueueForTab } from './stores';
export const handleError = async (errorResponse) => {
// The error can take on many shapes, so it should be normalized.
let err = '';
if (typeof errorResponse === 'string') {
err = errorResponse;
} else {
err = await errorResponse.text();
}
try {
// See if the error string can be parsed as JSON. If not, the block
// is exited before the `err` string is overwritten.
const parsed = JSON.parse(err);
err = parsed;
} catch {
// The catch behavior is established before the try block.
}
const errorMessage = err.message || err;
// The popup function expects an element, so a div containing the error
// message is created here for it to display in a modal.
const div = document.createElement('div');
const currentUrl =
window.location.pathname + window.location.search + window.location.hash;
if (err.unlock_url) {
try {
const unlockUrl = new URL(err.unlock_url, BASE_URL);
unlockUrl.searchParams.set('destination', currentUrl);
const updatedMessage = errorMessage.replace(
'[+ unlock link]',
`<a href="${
unlockUrl.pathname + unlockUrl.search
}" id="unlock-link">${Drupal.t('unlock link')}</a>`,
);
div.innerHTML += `<p>${updatedMessage}</p>`;
} catch {
div.innerHTML += `<p>${errorMessage}</p>`;
}
} else {
div.innerHTML += `<p>${errorMessage}</p>`;
}
openPopup(div, { title: 'Error while installing package(s)' });
};
/**
* Actives already-downloaded projects.
*
* @param {string[]} projectIds
* An array of project IDs to activate.
*
* @return {Promise<void>}
* A promise that resolves when the project is activated.
*/
export const activateProject = async (projectIds) => {
const url = `${BASE_URL}admin/modules/project_browser/activate`;
const installResponse = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(projectIds),
});
if (!installResponse.ok) {
await handleError(installResponse);
return;
}
try {
const responseContent = JSON.parse(await installResponse.text());
if (responseContent.hasOwnProperty('redirect')) {
window.location.href = responseContent.redirect;
}
} catch (err) {
await handleError(installResponse);
}
};
/**
* Performs the requests necessary to download and activate project via Package Manager.
*
* @param {string[]} projectIds
* An array of project IDs to download and activate.
*
* @return {Promise<void>}
* Returns a promise that resolves once the download and activation process is complete.
*/
export const doRequests = async (projectIds) => {
const beginInstallUrl = `${BASE_URL}admin/modules/project_browser/install-begin?source=${get(
activeTab,
)}`;
const beginInstallResponse = await fetch(beginInstallUrl);
if (!beginInstallResponse.ok) {
await handleError(beginInstallResponse);
} else {
const beginInstallData = await beginInstallResponse.json();
const stageId = beginInstallData.stage_id;
// The process of adding a module is separated into four stages, each
// with their own endpoint. When one stage completes, the next one is
// requested.
const installSteps = [
{
url: `${BASE_URL}admin/modules/project_browser/install-require/${stageId}`,
method: 'POST',
},
{
url: `${BASE_URL}admin/modules/project_browser/install-apply/${stageId}`,
method: 'GET',
},
{
url: `${BASE_URL}admin/modules/project_browser/install-post_apply/${stageId}`,
method: 'GET',
},
{
url: `${BASE_URL}admin/modules/project_browser/install-destroy/${stageId}`,
method: 'GET',
},
];
// eslint-disable-next-line no-restricted-syntax,guard-for-in
for (const step of installSteps) {
const options = {
method: step.method,
};
// Additional options need to be added when the request method is POST.
// This is specifically required for the `install-require` step.
if (step.method === 'POST') {
options.headers = {
'Content-Type': 'application/json',
};
// Set the request body to include the project(s) id as an array.
options.body = JSON.stringify(projectIds);
}
// eslint-disable-next-line no-await-in-loop
const stepResponse = await fetch(step.url, options);
if (!stepResponse.ok) {
// eslint-disable-next-line no-await-in-loop
const errorMessage = await stepResponse.text();
// eslint-disable-next-line no-console
console.warn(
`failed request to ${step.url}: ${errorMessage}`,
stepResponse,
);
// eslint-disable-next-line no-await-in-loop
await handleError(errorMessage);
return;
}
}
await activateProject(projectIds);
}
};
export const processQueue = async () => {
const currentQueueList = get(queueList)[get(activeTab)] || [];
const projectsToActivate = [];
const projectsToDownloadAndActivate = [];
for (const proj of currentQueueList) {
if (proj.status === 'absent') {
projectsToDownloadAndActivate.push(proj.id);
} else if (proj.status === 'present') {
projectsToActivate.push(proj.id);
}
}
document.body.style.pointerEvents = 'none';
if (projectsToActivate.length > 0) {
await activateProject(projectsToActivate);
}
if (projectsToDownloadAndActivate.length > 0) {
await doRequests(projectsToDownloadAndActivate);
}
document.body.style.pointerEvents = 'auto';
clearQueueForTab(get(activeTab));
for (const project of currentQueueList) {
project.status = 'active';
}
};
......@@ -20,3 +20,4 @@ export const DARK_COLOR_SCHEME =
export const ACTIVE_PLUGINS = drupalSettings.project_browser.active_plugins;
export const PACKAGE_MANAGER = drupalSettings.project_browser.package_manager;
export const FILTERS = drupalSettings.project_browser.filters || {};
export const MAX_SELECTIONS = drupalSettings.project_browser.max_selections;
project_browser.test_page:
path: '/project-browser/{source}'
defaults:
_controller: '\Drupal\project_browser_test\Controller\TestPageController::render'
_title: 'Project Browser Test Page'
requirements:
_permission: 'administer modules'
<?php
namespace Drupal\project_browser_test\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Returns a test page for Project Browser.
*/
class TestPageController extends ControllerBase {
/**
* Renders the Project Browser test page.
*
* @param string $source
* The ID of the source plugin to query for projects.
*
* @return array
* A render array.
*/
public function render(string $source): array {
return [
'#type' => 'project_browser',
'#max_selections' => 2,
'#source' => $source,
];
}
}
......@@ -76,10 +76,8 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
$cream_cheese_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(1)';
$download_button = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector button.project__action_button");
$this->assertNotEmpty($download_button);
$this->assertSame('Select Cream cheese on a bagel', $download_button->getText());
$this->assertSame('Install Cream cheese on a bagel', $download_button->getText());
$download_button->click();
$this->assertNotEmpty($assert_session->waitForButton('Install selected projects'));
$page->pressButton('Install selected projects');
$installed_action = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector .project_status-indicator", 30000);
$this->assertTrue($assert_session->waitForText('✓ Cream cheese on a bagel is Installed'));
$this->assertSame('✓ Cream cheese on a bagel is Installed', $installed_action->getText());
......@@ -104,10 +102,8 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
$pinky_brain_selector = '#project-browser .pb-layout__main ul > li:nth-child(2)';
$action_button = $assert_session->waitForElementVisible('css', "$pinky_brain_selector button.project__action_button");
$this->assertNotEmpty($action_button);
$this->assertSame('Select Pinky and the Brain', $action_button->getText());
$this->assertSame('Install Pinky and the Brain', $action_button->getText());
$action_button->click();
$this->assertNotEmpty($assert_session->waitForButton('Install selected projects'));
$page->pressButton('Install selected projects');
$popup = $assert_session->waitForElementVisible('css', '.project-browser-popup');
$this->assertNotEmpty($popup);
// The Pinky and the Brain module doesn't actually exist in the filesystem,
......@@ -148,9 +144,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
// Apply a recipe that ships with core.
$card = $assert_session->waitForElementVisible('css', '.pb-project:contains("Image media type")');
$this->assertNotEmpty($card);
$assert_session->buttonExists('Select', $card)->press();
$this->assertNotEmpty($assert_session->waitForButton('Install selected projects'));
$page->pressButton('Install selected projects');
$assert_session->buttonExists('Install', $card)->press();
$assert_installed($card);
// If we reload, the installation status should be remembered.
......@@ -168,9 +162,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
$assert_session->waitForElementVisible('css', ".search__search-submit")->click();
$card = $assert_session->waitForElementVisible('css', '.pb-project:contains("Test Recipe")');
$this->assertNotEmpty($card);
$assert_session->buttonExists('Select', $card)->press();
$this->assertNotEmpty($assert_session->waitForButton('Install selected projects'));
$page->pressButton('Install selected projects');
$assert_session->buttonExists('Install', $card)->press();
$field = $assert_session->waitForField('test_recipe[new_name]');
$this->assertNotEmpty($field);
$field->setValue('Y halo thar!');
......@@ -196,7 +188,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
$cream_cheese_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(1)';
$download_button = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector button.project__action_button");
$this->assertNotEmpty($download_button);
$this->assertSame('Select Cream cheese on a bagel', $download_button->getText());
$this->assertSame('Install Cream cheese on a bagel', $download_button->getText());
$this->drupalGet('/admin/config/development/project_browser');
$page->find('css', '#edit-allow-ui-install')->click();
$assert_session->checkboxNotChecked('edit-allow-ui-install');
......@@ -231,8 +223,6 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
$cream_cheese_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(1)';
$cream_cheese_button = $page->find('css', "$cream_cheese_module_selector button.project__action_button");
$cream_cheese_button->click();
$this->assertNotEmpty($assert_session->waitForButton('Install selected projects'));
$page->pressButton('Install selected projects');
$this->assertTrue($assert_session->waitForText('The process for adding projects is locked, but that lock has expired. Use unlock link to unlock the process and try to add the project again.'));
......@@ -242,8 +232,6 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
// Try beginning another install after breaking lock.
$cream_cheese_button = $page->find('css', "$cream_cheese_module_selector button.project__action_button");
$cream_cheese_button->click();
$this->assertNotEmpty($assert_session->waitForButton('Install selected projects'));
$page->pressButton('Install selected projects');
$installed_action = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector .project_status-indicator", 30000);
$assert_session->waitForText('✓ Cream cheese on a bagel is Installed');
$this->assertSame('✓ Cream cheese on a bagel is Installed', $installed_action->getText());
......@@ -274,8 +262,6 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
$cream_cheese_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(1)';
$cream_cheese_button = $page->find('css', "$cream_cheese_module_selector button.project__action_button");
$cream_cheese_button->click();
$this->assertNotEmpty($assert_session->waitForButton('Install selected projects'));
$page->pressButton('Install selected projects');
$this->assertTrue($assert_session->waitForText('The process for adding projects is locked, but that lock has expired. Use unlock link to unlock the process and try to add the project again.'));
// Click Unlock Install Stage link.
$this->clickWithWait('#ui-id-1 > p > a');
......@@ -283,8 +269,6 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
// Try beginning another install after breaking lock.
$cream_cheese_button = $page->find('css', "$cream_cheese_module_selector button.project__action_button");
$cream_cheese_button->click();
$this->assertNotEmpty($assert_session->waitForButton('Install selected projects'));
$page->pressButton('Install selected projects');
$installed_action = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector .project_status-indicator", 30000);
$assert_session->waitForText('✓ Cream cheese on a bagel is Installed');
$this->assertSame('✓ Cream cheese on a bagel is Installed', $installed_action->getText());
......@@ -326,10 +310,8 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
$cream_cheese_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(1)';
$download_button = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector button.project__action_button");
$this->assertNotEmpty($download_button);
$this->assertSame('Select Cream cheese on a bagel', $download_button->getText());
$this->assertSame('Install Cream cheese on a bagel', $download_button->getText());
$download_button->click();
$this->assertNotEmpty($assert_session->waitForButton('Install selected projects'));
$page->pressButton('Install selected projects');
$installed_action = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector .project_status-indicator", 30000);
$this->assertNotEmpty($installed_action);
$installed_action = $installed_action->waitFor(30, function ($button) {
......@@ -373,9 +355,8 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
public function testMultipleModuleAddAndInstall(): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$this->drupalGet('admin/modules/browse/project_browser_test_mock');
$this->drupalGet('project-browser/project_browser_test_mock');
$this->svelteInitHelper('text', 'Cream cheese on a bagel');
$this->svelteInitHelper('text', 'Kangaroo');
$assert_session->buttonNotExists('Install selected projects');
$cream_cheese_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(1)';
......@@ -386,6 +367,9 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
$was_selected = $select_button1->waitFor(10, fn ($button) => $button->getText() === 'Deselect Cream cheese on a bagel');
$this->assertTrue($was_selected);
$dancing_queen_button = $page->find('css', '#project-browser .pb-layout__main ul > li:nth-child(3) button');
$this->assertFalse($dancing_queen_button->hasAttribute('disabled'));
$this->assertNotEmpty($assert_session->waitForButton('Install selected projects'));
$kangaroo_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(4)';
......@@ -397,6 +381,8 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
return $button->getText() === 'Deselect Kangaroo';
});
$this->assertTrue($was_deselected);
// Select button gets disabled on reaching maximum limit.
$assert_session->elementAttributeExists('css', '#project-browser .pb-layout__main ul > li:nth-child(3) button.project__action_button', 'disabled');
$this->assertNotEmpty($assert_session->waitForButton('Install selected projects'));
$page->pressButton('Install selected projects');
......@@ -425,7 +411,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
public function testPluginSpecificQueue() {
$assert_session = $this->assertSession();
$this->container->get('module_installer')->install(['project_browser_devel'], TRUE);
$this->drupalGet('admin/modules/browse/project_browser_test_mock');
$this->drupalGet('project-browser/project_browser_test_mock');
$assert_session->buttonNotExists('Install selected projects');
$cream_cheese_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(1)';
......@@ -433,7 +419,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
$select_button1->click();
$this->assertNotEmpty($assert_session->waitForButton('Install selected projects'));
$this->drupalGet('admin/modules/browse/random_data');
$this->drupalGet('project-browser/random_data');
$assert_session->buttonNotExists('Install selected projects');
$random_data = '#project-browser .pb-layout__main ul > li:nth-child(2)';
$select_button2 = $assert_session->waitForElementVisible('css', "$random_data button.project__action_button");
......@@ -457,10 +443,8 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
$cream_cheese_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(1)';
$download_button = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector button.project__action_button");
$this->assertNotEmpty($download_button);
$this->assertSame('Select Cream cheese on a bagel', $download_button->getText());
$this->assertSame('Install Cream cheese on a bagel', $download_button->getText());
$download_button->click();
$this->assertNotEmpty($assert_session->waitForButton('Install selected projects'));
$page->pressButton('Install selected projects');
$unlock_url = $assert_session->waitForElementVisible('css', "#unlock-link")->getAttribute('href');
$this->assertStringEndsWith('/admin/modules/project_browser/install/unlock', parse_url($unlock_url, PHP_URL_PATH));
$query = parse_url($unlock_url, PHP_URL_QUERY);
......@@ -474,8 +458,8 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
*/
public function testSelectDeselectToggleInModal(): void {
$assert_session = $this->assertSession();
$this->drupalGet('admin/modules/browse/project_browser_test_mock');
$this->svelteInitHelper('text', 'Cream cheese on a bagel');
$this->drupalGet('project-browser/project_browser_test_mock');
$this->svelteInitHelper('text', 'Helvetica');
$assert_session->waitForButton('Helvetica')?->click();
// Click select button in modal.
$assert_session->elementExists('css', '.pb-detail-modal__sidebar_element button.project__action_button')->click();
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment