19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -116,7 +116,7 @@ test("can display client actions in Dialog and close the dialog", async () => {
expect(".modal .test_client_action").toHaveCount(1);
expect(".modal-title").toHaveText("Dialog Test");
await contains(".modal footer .btn.btn-primary").click();
await contains(".modal .btn-close").click();
expect(".modal .test_client_action").toHaveCount(0);
});
@ -498,3 +498,27 @@ test("test home client action", async () => {
await animationFrame();
expect.verifySteps(["/web/webclient/version_info", "assign /"]);
});
test("test display_exception client action", async () => {
expect.errors(1);
await mountWithCleanup(WebClient);
getService("action").doAction({
type: "ir.actions.client",
tag: "display_exception",
params: {
code: 0,
message: "Odoo Server Error",
data: {
name: `odoo.exceptions.UserError`,
debug: "traceback",
arguments: [],
context: {},
message: "This is an error",
},
},
});
await animationFrame();
expect(".o_dialog").toHaveCount(1);
expect("header .modal-title").toHaveText("Invalid Operation");
expect.verifyErrors([/RPC_ERROR/]);
});

View file

@ -15,9 +15,10 @@ import {
getKwArgs,
} from "@web/../tests/web_test_helpers";
import { mockTouch, runAllTimers } from "@odoo/hoot-mock";
import { animationFrame, mockTouch, runAllTimers } from "@odoo/hoot-mock";
import { browser } from "@web/core/browser/browser";
import { router } from "@web/core/browser/router";
import { router, routerBus } from "@web/core/browser/router";
import { rpcBus } from "@web/core/network/rpc";
import { user } from "@web/core/user";
import { WebClient } from "@web/webclient/webclient";
@ -147,7 +148,6 @@ class ResUsersSettings extends WebResUsersSettings {
} else {
ResUsersSettingsEmbeddedAction.write(embeddedSettings.id, vals);
}
return embeddedSettings;
}
}
@ -195,7 +195,7 @@ defineModels([
IrActionsAct_Window,
]);
defineActions([
const actions = [
{
id: 1,
xml_id: "action_1",
@ -264,10 +264,13 @@ defineActions([
parent_action_id: 4,
action_id: 4,
},
]);
];
defineActions(actions);
beforeEach(() => {
user.updateUserSettings("id", 1); // workaround to populate the user settings
user.updateUserSettings("embedded_actions_config_ids", {}); // workaround to populate the embedded user settings
});
test("can display embedded actions linked to the current action", async () => {
@ -311,6 +314,14 @@ test("can toggle visibility of embedded actions", async () => {
expect(".o_embedded_actions > button").toHaveCount(3, {
message: "Should have 2 embedded actions in the embedded + the dropdown button",
});
expect(user.settings.embedded_actions_config_ids).toEqual({
"1+": {
embedded_actions_order: [],
embedded_actions_visibility: [false, 102],
embedded_visibility: true,
res_model: "partner",
},
});
});
test("can click on a embedded action and execute the corresponding action (with xml_id)", async () => {
@ -441,6 +452,14 @@ test("a view coming from a embedded can be saved in the embedded actions", async
expect(".o_embedded_actions > button").toHaveCount(4, {
message: "Should have 2 embedded actions in the embedded + the dropdown button",
});
expect(user.settings.embedded_actions_config_ids).toEqual({
"1+": {
embedded_actions_order: [false, 102, 103, 4],
embedded_actions_visibility: [false, 102, 4],
embedded_visibility: true,
res_model: "partner",
},
});
});
test("a view coming from a embedded with python_method can be saved in the embedded actions", async () => {
@ -499,6 +518,14 @@ test("a view coming from a embedded with python_method can be saved in the embed
expect(".o_embedded_actions > button").toHaveCount(4, {
message: "Should have 2 embedded actions in the embedded + the dropdown button",
});
expect(user.settings.embedded_actions_config_ids).toEqual({
"1+": {
embedded_actions_order: [false, 102, 103, 4],
embedded_actions_visibility: [false, 103, 4],
embedded_visibility: true,
res_model: "partner",
},
});
});
test("the embedded actions should not be displayed when switching view", async () => {
@ -532,6 +559,14 @@ test("User can move the main (first) embedded action", async () => {
expect(".o_embedded_actions > button:nth-child(2) > span").toHaveText("Partners Action 1", {
message: "Main embedded action should've been moved to 2nd position",
});
expect(user.settings.embedded_actions_config_ids).toEqual({
"1+": {
embedded_actions_order: [102, false, 103],
embedded_actions_visibility: [false, 102],
embedded_visibility: true,
res_model: "partner",
},
});
});
test("User can unselect the main (first) embedded action", async () => {
@ -548,6 +583,14 @@ test("User can unselect the main (first) embedded action", async () => {
expect(dropdownItem).not.toHaveClass("selected", {
message: "Main embedded action should be unselected",
});
expect(user.settings.embedded_actions_config_ids).toEqual({
"1+": {
embedded_actions_order: [],
embedded_actions_visibility: [],
embedded_visibility: true,
res_model: "partner",
},
});
});
test("User should be redirected to the first embedded action set in user settings", async () => {
@ -636,3 +679,72 @@ test("custom embedded action loaded first", async () => {
message: "'Favorite Ponies' view should be loaded",
});
});
test("test get_embedded_actions_settings rpc args", async () => {
onRpc("res.users.settings", "get_embedded_actions_settings", ({ args, kwargs }) => {
expect(args.length).toBe(1, {
message: "Should have one positional argument, which is the id of the user setting.",
});
expect(args[0]).toBe(1, { message: "The id of the user setting should be 1." });
expect(kwargs.context.res_id).toBe(5, {
message: "The context should contain the res_id passed to the action.",
});
expect(kwargs.context.res_model).toBe("partner", {
message: "The context should contain the res_model passed to the action.",
});
expect.step("get_embedded_actions_settings");
});
await mountWithCleanup(WebClient);
await getService("action").doAction(1, {
additionalContext: { active_id: 5 },
});
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
expect.verifySteps(["get_embedded_actions_settings"]);
});
test("an action containing embedded actions should reload if the page is refreshed", async () => {
onRpc("create", ({ args }) => {
const values = args[0][0];
expect(values.name).toBe("Custom Partners Action 1");
expect(values.action_id).toBe(1);
// Add the created embedded action to the actions list so that the mock server knows it when reloading (/web/action/load)
defineActions([
...actions,
{
id: 4,
name: "Custom Partners Action 1",
parent_res_model: values.parent_res_model,
type: "ir.embedded.actions",
parent_action_id: 1,
action_id: values.action_id,
},
]);
return [4, values.name]; // Fake new embedded action id
});
onRpc(
"create_filter",
() => [5] // Fake new filter id
);
await mountWithCleanup(WebClient);
await getService("action").doAction(1);
// First, we create a new (custom) embedded action based on the current one
await contains(".o_control_panel_navigation > button > i.fa-sliders").click();
await waitFor(".o_popover.dropdown-menu");
await contains(".o_save_current_view ").click();
await contains(".o_save_favorite ").click();
expect(".o_embedded_actions > button").toHaveCount(3, {
message: "Should have 2 embedded actions in the embedded + the dropdown button",
});
// Emulate a hard refresh of the page
rpcBus.trigger("CLEAR-CACHES", "/web/action/load");
routerBus.trigger("ROUTE_CHANGE");
await animationFrame();
// Check that the created embedded action is still there, as the reload should be done
expect(".o_embedded_actions > button").toHaveCount(3, {
message:
"After refresh, we should still have 2 embedded actions in the embedded + the dropdown button",
});
});

View file

@ -1869,7 +1869,9 @@ describe(`new urls`, () => {
await mountWebClient();
await getService("action").doAction(100);
await runAllTimers(); // wait for the router to be updated
await contains(".o_data_cell").click();
await runAllTimers(); // wait for the router to be updated
await getService("action").doAction(200);
expect.verifySteps(["/web/action/load", "/web/action/load"]);

View file

@ -16,6 +16,7 @@ import {
import { animationFrame } from "@odoo/hoot-dom";
import { browser } from "@web/core/browser/browser";
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
import { router } from "@web/core/browser/router";
class Partner extends models.Model {
_name = "res.partner";
@ -81,6 +82,17 @@ test("open record withtout the correct company (doAction)", async () => {
});
});
const _pushState = router.pushState;
patchWithCleanup(router, {
pushState: (state, options) => {
expect(browser.location.href).toBe("https://www.hoot.test/");
const res = _pushState(state, options);
expect.step("pushState");
expect(browser.location.href).toBe("http://example.com/odoo/res.partner/1");
return res;
},
});
await mountWebClient();
getService("action").doAction({
type: "ir.actions.act_window",
@ -90,7 +102,7 @@ test("open record withtout the correct company (doAction)", async () => {
});
await animationFrame();
expect(cookie.get("cids")).toBe("1-2");
expect.verifySteps(["reload"]);
expect.verifySteps(["pushState", "reload"]);
expect(browser.location.href).toBe("http://example.com/odoo/res.partner/1", {
message: "url should contain the information of the doAction",
});

View file

@ -1,5 +1,6 @@
import { afterEach, expect, test } from "@odoo/hoot";
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
import { waitFor } from "@odoo/hoot-dom";
import {
contains,
defineActions,
@ -349,6 +350,54 @@ test("can use custom handlers for report actions", async () => {
]);
});
test("custom handlers can close modals", async () => {
defineActions([
{
id: 5,
name: "Create a Partner",
res_model: "partner",
target: "new",
views: [[false, "form"]],
},
]);
patchWithCleanup(download, {
_download: (options) => {
expect.step(options.url);
return Promise.resolve();
},
});
onRpc("/report/check_wkhtmltopdf", () => "ok");
await mountWithCleanup(WebClient);
registry.category("ir.actions.report handlers").add("custom_handler", async (action) => {
expect.step("calling custom handler for action " + action.id);
return true;
});
await getService("action").doAction(5);
await waitFor(".o_technical_modal .o_form_view");
expect(".o_technical_modal .o_form_view").toHaveCount(1, {
message: "should have rendered a form view in a modal",
});
await getService("action").doAction(7);
expect(".o_technical_modal .o_form_view").toHaveCount(1, {
message: "The modal should still exist",
});
await getService("action").doAction(11);
await animationFrame();
expect(".o_technical_modal .o_form_view").toHaveCount(0, {
message: "the modal should have been closed after the custom handler",
});
expect.verifySteps([
"calling custom handler for action 7",
"calling custom handler for action 11",
]);
});
test.tags("desktop");
test("context is correctly passed to the client action report", async (assert) => {
patchWithCleanup(download, {

View file

@ -1,4 +1,5 @@
import { expect, test } from "@odoo/hoot";
import { animationFrame, press } from "@odoo/hoot-dom";
import { Deferred } from "@odoo/hoot-mock";
import {
contains,
@ -62,8 +63,9 @@ test("Barcode scanner crop overlay", async () => {
patchWithCleanup(BarcodeVideoScanner.prototype, {
async isVideoReady() {
await super.isVideoReady(...arguments);
const result = await super.isVideoReady(...arguments);
videoReady.resolve();
return result;
},
onResize(overlayInfo) {
expect.step(overlayInfo);
@ -73,6 +75,7 @@ test("Barcode scanner crop overlay", async () => {
const firstBarcodeFound = scanBarcode(env);
await videoReady;
await animationFrame();
await contains(".o_crop_icon").dragAndDrop(".o_crop_container", {
relative: true,
position: {
@ -93,6 +96,7 @@ test("Barcode scanner crop overlay", async () => {
const secondBarcodeFound = scanBarcode(env);
await videoReady;
await animationFrame();
const secondValueScanned = await secondBarcodeFound;
expect(secondValueScanned).toBe(secondBarcodeValue, {
message: `The detected barcode (${secondValueScanned}) should be the same as generated (${secondBarcodeValue})`,
@ -135,3 +139,61 @@ test("BarcodeVideoScanner onReady props", async () => {
});
expect(await resolvedOnReadyPromise).toBe(true);
});
test("Closing barcode scanner before camera loads should not throw an error", async () => {
const env = await makeMockEnv();
await mountWithCleanup(WebClient, { env });
const cameraReady = new Deferred();
patchWithCleanup(browser.navigator, {
mediaDevices: {
async getUserMedia() {
await cameraReady;
const canvas = document.createElement("canvas");
return canvas.captureStream();
},
},
});
scanBarcode(env);
await animationFrame();
expect(".o-barcode-modal").toHaveCount(1)
await press("escape");
await animationFrame();
expect(".o-barcode-modal").toHaveCount(0)
cameraReady.resolve();
await animationFrame()
expect(".o_error_dialog").toHaveCount(0)
});
test("Closing barcode scanner while video is loading should not cause errors", async () => {
const env = await makeMockEnv();
await mountWithCleanup(WebClient, { env });
patchWithCleanup(browser.navigator, {
mediaDevices: {
async getUserMedia() {
const canvas = document.createElement("canvas");
return canvas.captureStream();
},
},
});
scanBarcode(env);
await animationFrame();
expect(".o-barcode-modal").toHaveCount(1)
await press("escape");
await animationFrame();
expect(".o-barcode-modal").toHaveCount(0)
await animationFrame()
expect(".o_error_dialog").toHaveCount(0)
});

View file

@ -180,6 +180,87 @@ test("clickbot clickeverywhere test", async () => {
]);
});
test("only one app", async () => {
onRpc("has_group", () => true);
mockDate("2017-10-08T15:35:11.000");
const clickEverywhereDef = new Deferred();
patchWithCleanup(browser.localStorage, {
removeItem(key) {
const savedState = super.getItem(key);
expect.step("savedState: " + savedState);
return super.removeItem(key);
},
});
patchWithCleanup(browser, {
console: {
log: (msg) => {
expect.step(msg);
if (msg === SUCCESS_SIGNAL) {
clickEverywhereDef.resolve();
}
},
error: (msg) => {
expect.step(msg);
clickEverywhereDef.resolve();
},
},
});
defineMenus([
{ id: 1, name: "App1", appID: 1, actionID: 1001, xmlid: "app1" },
{
id: 2,
children: [
{
id: 3,
name: "menu 1",
appID: 2,
actionID: 1002,
xmlid: "app2_menu1",
},
{
id: 4,
name: "menu 2",
appID: 2,
actionID: 1022,
xmlid: "app2_menu2",
},
],
name: "App2",
appID: 2,
actionID: 1002,
xmlid: "app2",
},
]);
const webClient = await mountWithCleanup(WebClient);
patchWithCleanup(odoo, {
__WOWL_DEBUG__: { root: webClient },
});
window.clickEverywhere("app1");
await clickEverywhereDef;
expect.verifySteps([
"Clicking on: apps menu toggle button",
"Testing app menu: app1",
"Testing menu App1 app1",
'Clicking on: menu item "App1"',
"Testing 2 filters",
'Clicking on: filter "Not Bar"',
'Clicking on: filter "Date"',
'Clicking on: filter option "October"',
"Testing view switch: kanban",
"Clicking on: kanban view switcher",
"Testing 2 filters",
'Clicking on: filter "Not Bar"',
'Clicking on: filter "Date"',
'Clicking on: filter option "October"',
"Successfully tested 1 apps",
"Successfully tested 0 menus",
"Successfully tested 0 modals",
"Successfully tested 4 filters",
SUCCESS_SIGNAL,
'savedState: {"light":false,"studioCount":0,"testedApps":["app1"],"testedMenus":["app1"],"testedFilters":4,"testedModals":0,"appIndex":0,"menuIndex":0,"subMenuIndex":0,"xmlId":"app1","app":"app1"}',
]);
});
test("clickbot clickeverywhere test (with dropdown menu)", async () => {
onRpc("has_group", () => true);
mockDate("2017-10-08T15:35:11.000");
@ -438,6 +519,7 @@ test("clickbot show rpc error when an error dialog is detected", async () => {
debug: "traceback",
arguments: [],
context: {},
message: "This is a server Error, it should be displayed in an error dialog",
},
exceptionName: "odoo.exceptions.Programming error",
subType: "server",
@ -457,7 +539,7 @@ test("clickbot show rpc error when an error dialog is detected", async () => {
<button class="btn btn-link p-0">See technical details</button>
</div>
</main>
<footer class="modal-footer justify-content-around justify-content-md-start flex-wrap gap-1 w-100">
<footer class="modal-footer d-empty-none justify-content-around justify-content-md-start flex-wrap gap-1 w-100">
<button class="btn btn-primary o-default-button">Close</button>
</footer>`
.trim()

View file

@ -4,6 +4,7 @@ import {
contains,
defineModels,
editSelectMenu,
getMockEnv,
mountView,
onRpc,
serverState,
@ -191,6 +192,7 @@ beforeEach(() => {
description: false,
group_ids: [1, 2],
category_id: 121,
placeholder: "No",
},
222: {
id: 222,
@ -198,6 +200,7 @@ beforeEach(() => {
description: "Project access rights description",
group_ids: [11, 12, 13],
category_id: 221,
placeholder: "View",
},
223: {
id: 223,
@ -273,6 +276,27 @@ test("simple rendering", async () => {
expect(".o_group_info_button").toHaveCount(0); // not displayed in non debug mode
});
test("simple rendering in readonly", async () => {
await mountView({
type: "form",
arch: `
<form edit="0">
<sheet>
<field name="group_ids" widget="res_user_group_ids"/>
</sheet>
</form>`,
resModel: "res.users",
resId: 1,
});
expect(".o_field_widget[name=group_ids] input").toHaveCount(0);
expect(queryAllTexts(".o_field_res_user_group_ids_privilege span")).toEqual([
"Access Rights",
"Project User",
"",
]);
});
test("simple rendering (debug)", async () => {
serverState.debug = "1";
await mountView({
@ -359,6 +383,34 @@ test("editing groups doesn't remove groups (debug)", async () => {
expect.verifySteps(["web_save"]);
});
test(`Click on "?" should not trigger a focus`, async () => {
await mountView({
type: "form",
arch: `
<form>
<sheet>
<field name="group_ids" widget="res_user_group_ids"/>
</sheet>
</form>`,
resModel: "res.users",
resId: 1,
});
expect(`.o_form_label[for="field_222_0"] :contains("?")`).toHaveCount(1);
await contains(`.o_form_label[for="field_222_0"] :contains("?")`).click();
await runAllTimers();
expect(".o-overlay-container .o-dropdown-item").toHaveCount(0);
if (getMockEnv().isSmall) {
expect(".o-overlay-container .o-tooltip").toHaveCount(1);
} else {
expect(".o-overlay-container .o-tooltip").toHaveCount(0);
}
await contains(`.o_form_label[for="field_222_0"]`).click();
await runAllTimers();
expect(".o-overlay-container .o-dropdown-item").toHaveCount(4);
expect(".o-overlay-container .o-tooltip").toHaveCount(0);
});
test.tags("desktop");
test(`privilege tooltips`, async () => {
await mountView({
@ -522,7 +574,7 @@ test("implied groups: lower level groups no longer available", async () => {
expect(".o_inner_group:eq(1) .o_select_menu").toHaveCount(2);
await contains(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).click();
expect(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).toHaveValue("Project User");
expect(".o_select_menu_item").toHaveCount(3);
expect(".o_select_menu_item").toHaveCount(4);
expect(".o_inner_group:eq(1) .o_wrap_input:last-child input").toHaveValue("");
await editSelectMenu(
".o_field_widget[name='group_ids'] .o_inner_group:nth-child(2) .o_wrap_input:last-child input",
@ -543,7 +595,7 @@ test("implied groups: lower level groups no longer available", async () => {
expect(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).toHaveValue("Project User");
await contains(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).click();
expect(".o_select_menu_item").toHaveCount(3);
expect(".o_select_menu_item").toHaveCount(4);
});
test("implied groups: lower level groups of same privilege still available", async () => {
@ -560,7 +612,7 @@ test("implied groups: lower level groups of same privilege still available", asy
resId: 1,
});
await contains(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).click();
expect(".o_select_menu_item").toHaveCount(3);
expect(".o_select_menu_item").toHaveCount(4);
});
test("do not lose shadowed groups when editing", async () => {
@ -738,3 +790,46 @@ test("privileges without category", async () => {
await contains(`.o_form_button_save`).click();
expect.verifySteps(["web_save"]);
});
test("privileges with placeholder", async () => {
ResUsers._records[0].group_ids = [];
await mountView({
type: "form",
arch: `
<form>
<sheet>
<field name="group_ids" widget="res_user_group_ids"/>
</sheet>
</form>`,
resModel: "res.users",
resId: 1,
});
expect(queryAllValues(".o_select_menu_input")).toEqual(["No", "View", ""]);
await contains(".o_field_widget[name=group_ids] .o_inner_group:eq(1) input").click();
expect(queryAllTexts(".o_select_menu_item")).toEqual([
"View",
"Project User",
"Project Manager",
"Project Administrator",
]);
await contains(".o_field_widget[name=group_ids] .o_inner_group:eq(1) input:eq(1)").click();
expect(`.o_select_menu_item`).toHaveCount(2);
await editSelectMenu(".o_field_widget[name=group_ids] .o_inner_group:eq(1) input:eq(1)", {
value: "Helpdesk Administrator",
});
expect(queryAllValues(".o_select_menu_input")).toEqual(["No", "", "Helpdesk Administrator"]);
expect(queryFirst(".o_inner_group:eq(1) .o_wrap_input input")).toHaveAttribute(
"placeholder",
"Project Manager"
);
await contains(".o_field_widget[name=group_ids] .o_inner_group:eq(1) input").click();
expect(queryAllTexts(".o_select_menu_item")).toEqual([
"Project Manager",
"Project Administrator",
]);
});

View file

@ -2083,7 +2083,7 @@ test("server actions are called with the correct context", async () => {
test("BinaryField is correctly rendered in Settings form view", async () => {
onRpc("/web/content", async (request) => {
const body = await request.text();
const body = await request.formData();
expect(body).toBeInstanceOf(FormData);
expect(body.get("field")).toBe("file", {
message: "we should download the field document",
@ -2369,3 +2369,27 @@ test("settings search is accent-insensitive", async () => {
await editSearch("àz");
expect(queryAllTexts(".highlighter")).toEqual(["ÄZ", "áz"]);
});
test("settings search does not highlight escaped characters when highlighting the searched text", async () => {
await mountView({
type: "form",
resModel: "res.config.settings",
arch: /* xml */ `
<form string="Settings" class="oe_form_configuration o_base_settings" js_class="base_settings">
<app string="CRM" name="crm">
<block title="Research &amp; Development">
<setting help="This is Research &amp; Development Settings">
<field name="bar"/>
<div>This test is to check whether &amp; gets escaped during search or not.</div>
</setting>
</block>
</app>
</form>
`,
});
await editSearch("a");
expect(queryAllTexts(".highlighter")).toEqual(["a", "a", "a", "a", "a"]);
await editSearch("&");
expect(queryAllTexts(".highlighter")).toEqual(["&", "&", "&"]);
});

View file

@ -644,3 +644,41 @@ test("de-select only changes visible companies", async () => {
1
);
});
test("disallowed companies in between allowed companies are not enabled", async () => {
cookie.set("cids", "3");
serverState.companies = [
{ id: 1, name: "Parent", sequence: 1, parent_id: false, child_ids: [2] },
{ id: 2, name: "Child A", sequence: 2, parent_id: 1, child_ids: [3] },
{ id: 3, name: "Child B", sequence: 3, parent_id: 2, child_ids: [] },
];
patchWithCleanup(user.allowedCompanies, [serverState.companies[0], serverState.companies[2]]);
await createSwitchCompanyMenu();
/**
* [ ] Parent
* [ ] Child A
* [x] Child B
*/
expect(user.activeCompanies.map((c) => c.id)).toEqual([3]);
expect(user.activeCompany.id).toBe(3);
await openCompanyMenu();
expect("[data-company-id]").toHaveCount(3);
expect("[data-company-id] .fa-check-square").toHaveCount(1);
expect("[data-company-id] .fa-square-o").toHaveCount(2);
/**
* [x] Parent -> toggle
* [ ] Child A
* [x] Child B
*/
await contains(".log_into:eq(0)").click();
expect(cookie.get("cids")).toEqual("1-3");
await openCompanyMenu();
await toggleCompany(0);
expect("[data-company-id] .fa-check-square").toHaveCount(0);
expect("[data-company-id] .fa-square-o").toHaveCount(3);
});