discourse/app/assets/javascripts/admin/addon/components/bulk-user-delete-confirmation.gjs
Joffrey JAFFEUX b6aad28ccf
DEV: replace selenium driver with playwright (#31977)
This commit is replacing the system specs driver (selenium) by
Playwright: https://playwright.dev/

We are still using Capybara to write the specs but they will now be run
by Playwright. To achieve this we are using the non official ruby
driver: https://github.com/YusukeIwaki/capybara-playwright-driver

### Notable changes

- `CHROME_DEV_TOOLS` has been removed, it's not working well with
playwright use `pause_test` and inspect browser for now.

- `fill_in` is not generating key events in playwright, use `send_keys`
if you need this.

### New spec options

#### trace

Allows to capture a trace in a zip file which you can load at
https://trace.playwright.dev or locally through `npx playwright
show-trace /path/to/trace.zip`

_Example usage:_

```ruby
it "shows bar", trace: true do
  visit("/")

  find(".foo").click

  expect(page).to have_css(".bar")
end
```

#### video

Allows to capture a video of your spec.

_Example usage:_

```ruby
it "shows bar", video: true do
  visit("/")

  find(".foo").click

  expect(page).to have_css(".bar")
end
```

### New env variable

#### PLAYWRIGHT_SLOW_MO_MS

Allow to force playwright to wait DURATION (in ms) at each action.

_Example usage:_

```
PLAYWRIGHT_SLOW_MO_MS=1000 rspec foo_spec.rb
```

#### PLAYWRIGHT_HEADLESS

Allow to be in headless mode or not. Default will be headless.

_Example usage:_

```
PLAYWRIGHT_HEADLESS=0 rspec foo_spec.rb # will show the browser
```

### New helpers

#### with_logs

Allows to access the browser logs and check if something specific has
been logged.

_Example usage:_

```ruby
with_logs do |logger|
  # do something

  expect(logger.logs.map { |log| log[:message] }).to include("foo")
end
```

#### add_cookie

Allows to add a cookie on the browser session.

_Example usage:_

```ruby
add_cookie(name: "destination_url", value: "/new")
```

#### get_style

Get the property style value of an element.

_Example usage:_

```ruby
expect(get_style(find(".foo"), "height")).to eq("200px")
```

#### get_rgb_color

Get the rgb color of an element.

_Example usage:_

```ruby
expect(get_rgb_color(find("html"), "backgroundColor")).to eq("rgb(170, 51, 159)")
```
2025-05-06 10:44:14 +02:00

213 lines
5.6 KiB
Text
Vendored

import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { TrackedArray } from "@ember-compat/tracked-built-ins";
import { modifier as modifierFn } from "ember-modifier";
import DButton from "discourse/components/d-button";
import DModal from "discourse/components/d-modal";
import { ajax } from "discourse/lib/ajax";
import { extractError } from "discourse/lib/ajax-error";
import { bind } from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
const BULK_DELETE_CHANNEL = "/bulk-user-delete";
export default class BulkUserDeleteConfirmation extends Component {
@service messageBus;
@tracked confirmButtonDisabled = true;
@tracked deleteStarted = false;
@tracked logs = new TrackedArray();
failedUsernames = [];
callAfterBulkDelete = false;
blockIpAndEmail = false;
logsListener = modifierFn(() => {
this.messageBus.subscribe(
BULK_DELETE_CHANNEL,
this.onDeleteProgress,
this.args.model.lastBulkDeleteMessageBusId
);
return () => {
this.messageBus.unsubscribe(BULK_DELETE_CHANNEL, this.onDeleteProgress);
};
});
get confirmDeletePhrase() {
return i18n(
"admin.users.bulk_actions.delete.confirmation_modal.confirmation_phrase",
{ count: this.args.model.userIds.length }
);
}
#logError(line) {
this.#log(line, "error");
}
#logSuccess(line) {
this.#log(line, "success");
}
#logNeutral(line) {
this.#log(line, "neutral");
}
#log(line, type) {
this.logs.push({
line,
type,
});
}
@bind
onDeleteProgress(data) {
if (data.success) {
this.#logSuccess(
i18n(
"admin.users.bulk_actions.delete.confirmation_modal.user_delete_succeeded",
data
)
);
} else if (data.failed) {
this.failedUsernames.push(data.username);
this.#logError(
i18n(
"admin.users.bulk_actions.delete.confirmation_modal.user_delete_failed",
data
)
);
}
if (data.position === data.total) {
this.callAfterBulkDelete = true;
this.#logNeutral(
i18n(
"admin.users.bulk_actions.delete.confirmation_modal.bulk_delete_finished"
)
);
if (this.failedUsernames.length > 0) {
this.#logNeutral(
i18n(
"admin.users.bulk_actions.delete.confirmation_modal.failed_to_delete_users"
)
);
for (const username of this.failedUsernames) {
this.#logNeutral(`* ${username}`);
}
}
}
}
@action
onPromptInput(event) {
this.confirmButtonDisabled =
event.target.value.toLowerCase() !== this.confirmDeletePhrase;
}
@action
async startDelete() {
this.deleteStarted = true;
this.confirmButtonDisabled = true;
this.#logNeutral(
i18n(
"admin.users.bulk_actions.delete.confirmation_modal.bulk_delete_starting"
)
);
try {
await ajax("/admin/users/destroy-bulk.json", {
type: "DELETE",
data: {
user_ids: this.args.model.userIds,
block_ip_and_email: this.blockIpAndEmail,
},
});
this.callAfterBulkDelete = true;
} catch (err) {
this.#logError(extractError(err));
this.confirmButtonDisabled = false;
}
}
@action
closeModal() {
this.args.closeModal();
if (this.callAfterBulkDelete) {
this.args.model?.afterBulkDelete();
}
}
@action
toggleBlockIpAndEmail(event) {
this.blockIpAndEmail = event.target.checked;
}
<template>
<DModal
class="bulk-user-delete-confirmation"
@closeModal={{this.closeModal}}
@title={{i18n
"admin.users.bulk_actions.delete.confirmation_modal.title"
count=@model.userIds.length
}}
{{this.logsListener}}
>
<:body>
{{#if this.deleteStarted}}
<div class="bulk-user-delete-confirmation__progress">
{{#each this.logs as |entry|}}
<div
class="bulk-user-delete-confirmation__progress-line -{{entry.type}}"
>
{{entry.line}}
</div>
{{/each}}
<div class="bulk-user-delete-confirmation__progress-anchor">
</div>
</div>
{{else}}
<p>{{i18n
"admin.users.bulk_actions.delete.confirmation_modal.prompt_text"
count=@model.userIds.length
confirmation_phrase=this.confirmDeletePhrase
}}
</p>
<input
class="confirmation-phrase"
type="text"
placeholder={{this.confirmDeletePhrase}}
{{on "input" this.onPromptInput}}
/>
<label class="checkbox-label">
<input
type="checkbox"
class="block-ip-and-email"
{{on "change" this.toggleBlockIpAndEmail}}
/>
{{i18n
"admin.users.bulk_actions.delete.confirmation_modal.block_ip_and_email"
}}
</label>
{{/if}}
</:body>
<:footer>
<DButton
class="confirm-delete btn-danger"
@icon="trash-can"
@label="admin.users.bulk_actions.delete.confirmation_modal.confirm"
@disabled={{this.confirmButtonDisabled}}
@action={{this.startDelete}}
/>
<DButton
class="btn-default"
@label="admin.users.bulk_actions.delete.confirmation_modal.close"
@action={{this.closeModal}}
/>
</:footer>
</DModal>
</template>
}