HOOT

نمای کلی

HOOT یک چارچوب تست است که با Owl نوشته شده و ویژگی‌های کلیدی آن عبارتند از:

  • ثبت و اجرای تست‌ها و test suiteها؛

  • نمایش رابطی شهودی برای مشاهده و فیلتر کردن نتایج تست؛

  • ارائهٔ راه‌هایی برای تعامل با DOM به‌منظور شبیه‌سازی اقدامات کاربر؛

  • ارائهٔ کمک‌کننده‌های سطح پایین که اجازهٔ mock کردن اشیای سراسری مختلف را می‌دهند.

از این رو، به‌عنوان یک lib/ در کدبیس Odoo ادغام شده و دو ماژول اصلی را صادر می‌کند:

  • @odoo/hoot-dom: (قابل استفاده در tourها) کمک‌کننده‌هایی برای:

  • @odoo/hoot: (فقط در تست‌های واحد استفاده می‌شود) تمام ویژگی‌های چارچوب تست:

    • test، describe و expect

    • هوک‌های تست مانند after و afterEach

    • مدیریت fixture با getFixture

    • مدیریت تاریخ و زمان مانند mockDate یا advanceTime

    • mock کردن پاسخ‌های شبکه از طریق mockFetch() یا mockWebSocket()

    • هر helper که توسط @odoo/hoot-dom صادر شده است

توجه

این بخش از مستندات قرار نیست تمام helperهای موجود در Hoot را فهرست کند (فهرست کامل را می‌توان در خود ماژول @odoo/hoot یافت). هدف در اینجا نشان دادن پرکاربردترین helperها و توجیه برخی از تصمیماتی است که به شکل فعلی چارچوب تست منجر شده‌اند.

Running tests

در Odoo، تست‌های واحد frontend را می‌توان با رفتن به URL /web/tests اجرا کرد. بیشتر تنظیمات لازم برای فراخوانی test runner از پیش انجام شده است:

  • bundle web.assets_unit_tests از پیش تعریف شده است و همهٔ تست‌های تعریف‌شده در بیشتر افزونه‌ها را شناسایی می‌کند؛

  • فایل start.hoot.js مسئول فراخوانی test runner با تابع نقطهٔ ورود صادرشدهٔ start آن است.

هنگام رفتن به صفحهٔ تست، تست‌ها به‌صورت ترتیبی اجرا شده و نتایج در کنسول و در GUI نمایش داده می‌شوند (در صورتی که در حالت headless اجرا نشود).

گزینه‌های Runner

runner را می‌توان به یکی از روش‌های زیر پیکربندی کرد:

  • از طریق رابط کاربری (با منوی کشویی پیکربندی و نوار جستجو)؛

  • یا از طریق پارامترهای query در URL (مثلاً ?headless برای اجرا در حالت headless).

فهرست گزینه‌های موجود برای runner در ادامه آمده است:

  • bail

    تعداد تست‌های ناموفقی که پس از آن test runner متوقف می‌شود. مقدار falsy (شامل 0) یعنی runner هرگز متوقف نشود. (پیش‌فرض: 0)

  • debugTest

    مشابه فیلتر FILTER_SCHEMA.test، اما test runner را نیز در حالت «debug» قرار می‌دهد. برای اطلاعات بیشتر به TestRunner.debug مراجعه کنید. (پیش‌فرض: false)

  • fps

    مقدار frames per seconds را تنظیم می‌کند (به میلی‌ثانیه تبدیل شده و در advanceFrame استفاده می‌شود)

  • filter

    رشتهٔ جستجو که تست‌ها/suiteهای مطابق را بر اساس نام کامل آنها (شامل suite(های) والد) و برچسب‌هایشان فیلتر می‌کند. (پیش‌فرض: "")

  • frameRate

    مقدار تخمینی فریم‌های رندرشده در ثانیه، که هنگام mock کردن فریم‌های انیمیشن استفاده می‌شود. (پیش‌فرض: 60 fps)

  • fun

    حال‌وهوا را شاد می‌کند. (پیش‌فرض: false)

  • headless

    آیا رابط کاربری test runner رندر شود یا خیر. (پیش‌فرض: false)

  • id

    شناسه‌های suiteها یا تست‌هایی که به‌صورت انحصاری اجرا می‌شوند. شناسهٔ یک job به‌صورت قطعی بر اساس نام کامل آن تولید می‌شود.

  • loglevel

    سطح log مورد استفادهٔ test runner. هرچه سطح بالاتر باشد، logهای بیشتری نمایش داده می‌شود:

    • 0: فقط logهای runner نمایش داده می‌شود (پیش‌فرض)

    • 1: نتایج همهٔ suiteها نیز ثبت می‌شود

    • 2: نتایج همهٔ تست‌ها نیز ثبت می‌شود

    • 3: اطلاعات debug برای هر تست نیز ثبت می‌شود

  • manual

    اینکه آیا test runner باید پس از بارگذاری صفحه به‌صورت دستی شروع شود (پیش‌فرض شروع خودکار است). (پیش‌فرض: false)

  • notrycatch

    ایمنی دستورات try .. catch پیرامون تابع اجرای هر تست را حذف می‌کند تا اجازه دهد خطاها به مرورگر منتقل شوند. (پیش‌فرض: false)

  • order

    ترتیب اجرای تست‌ها را تعیین می‌کند:

    • "fifo": تست‌ها به‌صورت ترتیبی همان‌طور که در سیستم فایل اعلام شده‌اند اجرا می‌شوند؛

    • "lifo": تست‌ها به‌صورت ترتیبی به ترتیب معکوس اجرا می‌شوند؛

    • "random": تست‌ها و suiteها در داخل suite والد خود به‌صورت تصادفی مرتب می‌شوند.

  • preset

    محیطی که test runner در آن اجرا می‌شود. این پارامتر برای تعیین مقدار پیش‌فرض سایر ویژگی‌ها استفاده می‌شود، یعنی:

    • user agent؛

    • پشتیبانی از لمس؛

    • اندازهٔ موردانتظار viewport.

  • showdetail

    نحوهٔ نمایش جزئیات تست‌های ناموفق در رابط کاربری را تعیین می‌کند. (پیش‌فرض: "first-fail")

  • tag

    نام برچسب‌های تست‌ها و suiteهایی که به‌صورت انحصاری اجرا می‌شوند (غیرحساس به حروف). (پیش‌فرض: خالی)

  • timeout

    مدت‌زمانی (به میلی‌ثانیه) که در پایان آن، یک تست به‌صورت خودکار شکست می‌خورد. (پیش‌فرض: 5 ثانیه)

توجه

هنگام انتخاب تست‌ها و suiteها برای اجرا، یک OR ضمنی بین فیلترهای شامل‌کننده اعمال می‌شود. این بدان معنی است که افزودن فیلترهای شامل‌کنندهٔ بیشتر منجر به اجرای تست‌های بیشتر می‌شود. این مورد برای فیلترهای filter، id و tag صدق می‌کند (در عوض فیلترهای حذف‌کننده تست‌های مطابق را از فهرست تست‌های قابل اجرا حذف می‌کنند).

نوشتن تست‌ها

آزمایش

نوشتن یک تست می‌تواند بسیار ساده باشد، چرا که صرفاً به فراخوانی تابع test با یک نام و یک تابع که حاوی منطق تست است نیاز دارد.

در اینجا یک مثال ساده آورده شده است:

import { expect, test } from "@odoo/hoot";

test("My first test", () => {
    expect(2 + 2).toBe(4);
});

Describe

بیشتر اوقات، تست‌ها به این سادگی نیستند. اغلب نیاز به مقداری setup و teardown دارند و گاهی لازم است در قالب یک suite گروه‌بندی شوند. اینجاست که تابع describe وارد عمل می‌شود.

به این صورت می‌توانید یک suite و یک تست درون آن را اعلام کنید:

import { describe, expect, test } from "@odoo/hoot";

describe("My first suite", () => {
    test("My first test", () => {
        expect(2 + 2).toBe(4);
    });
});

مهم

در Odoo، تمام فایل‌های تست در یک محیط ایزوله اجرا می‌شوند و درون یک بلوک describe سراسری بسته‌بندی می‌گردند (نام suite برابر با مسیر فایل تست است).

با در نظر گرفتن این موضوع، نیازی به اعلام یک suite در فایل‌های تست خود ندارید، هرچند همچنان می‌توانید در همان فایل sub-suiteها را اعلام کنید، اگر می‌خواهید برای اهداف سازماندهی یا برچسب‌گذاری suite فایل را تقسیم کنید.

Expect

تابع expect تابع assertion اصلی این چارچوب است. برای assertion این مورد استفاده می‌شود که یک مقدار یا شیء همان چیزی است که انتظار می‌رود یا در وضعیتی است که قرار است باشد. برای این کار، چند modifier و طیف وسیعی از matcherها فراهم می‌کند.

Modifierها

یک modifier از expect یک getter است که مجموعهٔ دیگری از matcherهای تغییریافته را که به‌صورت خاصی رفتار می‌کنند، برمی‌گرداند.

  • not

    نتیجهٔ matcher بعدی را معکوس می‌کند: در صورتی موفق می‌شود که matcher شکست بخورد.

    expect(true).not.toBe(false);
    
  • resolves

    منتظر می‌ماند تا مقدار (Promise) «resolve» شود سپس matcher بعدی را با مقدار resolve‌شده اجرا می‌کند.

    await expect(Promise.resolve(42)).resolves.toBe(42);
    
  • rejects

    منتظر می‌ماند تا مقدار (Promise) «reject» شود سپس matcher بعدی را با دلیل reject اجرا می‌کند.

    await expect(Promise.reject("error")).rejects.toBe("error");
    

توجه

modifierهای resolves و rejects تنها زمانی در دسترس هستند که مقدار یک promise باشد و یک promise بازمی‌گردانند که پس از انجام assertion، resolve می‌شود.

matcherهای معمولی

matcherها تعیین می‌کنند که با مقدار در حال تست چه کاری انجام شود. برخی آن مقدار را همان‌گونه که هست می‌گیرند، در حالی که برخی دیگر آن مقدار را پیش از انجام assertion روی آن تبدیل می‌کنند (مثلاً matcherهای DOM).

توجه داشته باشید که پارامتر آرگومان آخر تمام matcherها یک دیکشنری اختیاری با گزینه‌های اضافی است که در آن می‌توان یک message سفارشی برای assertion ارائه داد تا کانتکست/خاص‌بودگی بیشتری اضافه شود.

فهرست اول matcherها بر پایهٔ primitive یا object هستند و رایج‌ترین موارد محسوب می‌شوند:

toBe(expected[, options])

انتظار دارد مقدار دریافت‌شده دقیقاً برابر با مقدار expected باشد.

  • پارامترها

    • expected: any

    • options: { message?: string }

  • مثال‌ها

    expect("foo").toBe("foo");
    expect({ foo: 1 }).not.toBe({ foo: 1 });
    
toBeCloseTo(expected[, options])

انتظار دارد مقدار دریافت‌شده تا تعداد معینی رقم نزدیک به مقدار expected باشد (پیش‌فرض 2 است).

  • پارامترها

    • expected: any

    • options: { message?: string, digits?: number }

  • مثال‌ها

    expect(0.2 + 0.1).toBeCloseTo(0.3);
    expect(3.51).toBeCloseTo(3.5, { digits: 1 });
    
toBeEmpty([options])

انتظار دارد مقدار دریافت‌شده خالی باشد:

  • iterable: بدون آیتم

  • object: بدون کلید

  • node: بدون محتوا (یعنی بدون مقدار یا متن)

  • هر چیز دیگر: مقدار falsy (false، 0، ""، null، undefined)

  • پارامترها

    • options: { message?: string }

  • مثال‌ها

    expect({}).toBeEmpty();
    expect(["a", "b"]).not.toBeEmpty();
    expect(queryOne("input")).toBeEmpty();
    
toBeGreaterThan(min[, options])

انتظار دارد مقدار دریافت‌شده دقیقاً بزرگ‌تر از min باشد.

  • پارامترها

    • min: number

    • options: { message?: string }

  • مثال‌ها

    expect(5).toBeGreaterThan(-1);
    expect(4 + 2).toBeGreaterThan(5);
    
toBeInstanceOf(cls[, options])

انتظار دارد مقدار دریافت‌شده نمونه‌ای از cls داده‌شده باشد.

  • پارامترها

    • cls: Function

    • options: { message?: string }

  • مثال‌ها

    expect({ foo: 1 }).not.toBeInstanceOf(Object);
    expect(document.createElement("div")).toBeInstanceOf(HTMLElement);
    
toBeLessThan(max[, options])

انتظار دارد مقدار دریافت‌شده دقیقاً کوچک‌تر از max باشد.

  • پارامترها

    • max: number

    • options: { message?: string }

  • مثال‌ها

    expect(5).toBeLessThan(10);
    expect(8 - 6).toBeLessThan(3);
    
toBeOfType(type[, options])

انتظار دارد مقدار دریافت‌شده از type داده‌شده باشد.

  • پارامترها

    • type: string

    • options: { message?: string }

  • مثال‌ها

    expect("foo").toBeOfType("string");
    expect({ foo: 1 }).toBeOfType("object");
    
toBeWithin(min, max[, options])

انتظار دارد مقدار دریافت‌شده بین min و max (هر دو شامل) باشد.

  • پارامترها

    • min: number

    • max: number

    • options: { message?: string }

  • مثال‌ها

    expect(3).toBeWithin(3, 9);
    expect(-8.5).toBeWithin(-20, 0);
    expect(100).toBeWithin(50, 100);
    
toEqual(expected[, options])

انتظار دارد مقدار دریافت‌شده به‌صورت عمیق برابر با مقدار expected باشد.

  • پارامترها

    • expected: any

    • options: { message?: string }

  • مثال‌ها

    expect(["foo"]).toEqual(["foo"]);
    expect({ foo: 1 }).toEqual({ foo: 1 });
    
toHaveLength(length[, options])

انتظار دارد مقدار دریافت‌شده طولی برابر با length داده‌شده داشته باشد. مقدار دریافت‌شده می‌تواند هر Iterable یا Object باشد.

  • پارامترها

    • length: number

    • options: { message?: string }

  • مثال‌ها

    expect("foo").toHaveLength(3);
    expect([1, 2, 3]).toHaveLength(3);
    expect({ foo: 1, bar: 2 }).toHaveLength(2);
    expect(new Set([1, 2])).toHaveLength(2);
    
toInclude(item[, options])

انتظار دارد مقدار دریافت‌شده یک item با شکل داده‌شده را شامل شود.

مقدار دریافت‌شده می‌تواند یک iterable یا یک object باشد (در صورت object بودن، item باید یک کلید یا یک tuple باشد که یک ورودی در آن object را نشان می‌دهد).

توجه داشته باشید که این یک مقایسهٔ سخت‌گیرانه نیست: آیتم برای برابری عمیق در برابر هر آیتم از iterable مطابقت داده می‌شود.

  • پارامترها

    • item: any

    • options: { message?: string }

  • مثال‌ها

    expect([1, 2, 3]).toInclude(2);
    expect({ foo: 1, bar: 2 }).toInclude("foo");
    expect({ foo: 1, bar: 2 }).toInclude(["foo", 1]);
    expect(new Set([{ foo: 1 }, { bar: 2 }])).toInclude({ bar: 2 });
    
toMatch(matcher[, options])

انتظار دارد مقدار دریافت‌شده با matcher داده‌شده مطابقت داشته باشد.

  • پارامترها

    • matcher: string | number | RegExp

    • options: { message?: string }

  • مثال‌ها

    expect(new Error("foo")).toMatch("foo");
    expect("a foo value").toMatch(/fo.*ue/);
    
toThrow(matcher[, options])

انتظار دارد Function دریافت‌شده پس از فراخوانی یک خطا پرتاب کند.

  • پارامترها

    • matcher: string | number | RegExp

    • options: { message?: string }

  • مثال‌ها

    expect(() => { throw new Error("Woops!") }).toThrow(/woops/i);
    await expect(Promise.reject("foo")).rejects.toThrow("foo");
    

matcherهای DOM

فهرست بعدی matcherها بر پایهٔ node هستند و برای assertion وضعیت یک node یا فهرستی از nodeها استفاده می‌شوند. آنها معمولاً یک سلکتور سفارشی را به‌عنوان آرگومان تابع expect می‌گیرند (هرچند یک Node یا iterable از Node نیز پذیرفته می‌شود).

toBeChecked([options])

انتظار دارد Target دریافت‌شده "checked" باشد یا اگر گزینهٔ هم‌نام روی true تنظیم شده باشد، "indeterminate" باشد.

  • پارامترها

    • options: { message?: string, indeterminate?: boolean }

  • مثال‌ها

    expect("input[type=checkbox]").toBeChecked();
    
toBeDisplayed([options])

انتظار دارد Target دریافت‌شده «displayed» باشد، به این معنی که:

  • یک bounding box دارد؛

  • در document ریشه قرار دارد.

  • پارامترها

    • options: { message?: string }

  • مثال‌ها

    expect(document.body).toBeDisplayed();
    expect(document.createElement("div")).not.toBeDisplayed();
    
toBeEnabled([options])

انتظار دارد Target دریافت‌شده «enabled» باشد، به این معنی که با pseudo-selector :enabled مطابقت دارد.

  • پارامترها

    • options: { message?: string }

  • مثال‌ها

    expect("button").toBeEnabled();
    expect("input[type=radio]").not.toBeEnabled();
    
toBeFocused([options])

انتظار دارد Target دریافت‌شده در document مالک خود «focused» باشد.

  • پارامترها

    • options: { message?: string }

toBeVisible([options])

انتظار دارد Target دریافت‌شده «visible» باشد، به این معنی که:

  • یک bounding box دارد؛

  • در document ریشه قرار دارد؛

  • توسط ویژگی‌های CSS پنهان نشده است.

  • پارامترها

    • options: { message?: string }

  • مثال‌ها

    expect(document.body).toBeVisible();
    expect("[style='opacity: 0']").not.toBeVisible();
    
toHaveAttribute(attribute, value[, options])

انتظار دارد Target دریافت‌شده ویژگی داده‌شده را تنظیم‌شده داشته باشد و اگر valueای داده شده باشد، مقدار آن ویژگی با آن مطابقت داشته باشد.

  • پارامترها

    • attribute: string

    • value: string | number | RegExp

    • options: { message?: string }

  • مثال‌ها

    expect("a").toHaveAttribute("href");
    expect("script").toHaveAttribute("src", "./index.js");
    
toHaveClass(className[, options])

انتظار دارد Target دریافت‌شده نام‌(های) کلاس داده‌شده را داشته باشد.

  • پارامترها

    • className: string | string[]

    • options: { message?: string }

  • مثال‌ها

    expect("button").toHaveClass("btn btn-primary");
    expect("body").toHaveClass(["o_webclient", "o_dark"]);
    
toHaveCount(amount[, options])

انتظار دارد Target دریافت‌شده دقیقاً amount عنصر را شامل شود. توجه داشته باشید که پارامتر amount را می‌توان حذف کرد، در این صورت تابع حداقل یک عنصر را انتظار خواهد داشت.

  • پارامترها

    • amount: number

    • options: { message?: string }

  • مثال‌ها

    expect(".o_webclient").toHaveCount(1);
    expect(".o_form_view .o_field_widget").toHaveCount();
    expect("ul > li").toHaveCount(4);
    
toHaveInnerHTML(expected[, options])

انتظار دارد innerHTML مربوط به Target دریافت‌شده با مقدار expected مطابقت داشته باشد (پس از قالب‌بندی).

  • پارامترها

    • expected: string | RegExp

    • options: { message?: string, type?: "html" | "xml", tabSize?: number, keepInlineTextNodes?: boolean }

  • مثال‌ها

    expect(".my_element").toHaveInnerHTML(`
        Some <strong>text</strong>
    `);
    
toHaveOuterHTML(expected[, options])

انتظار دارد outerHTML مربوط به Target دریافت‌شده با مقدار expected مطابقت داشته باشد (پس از قالب‌بندی).

  • پارامترها

    • expected: string | RegExp

    • options: { message?: string, type?: "html" | "xml", tabSize?: number, keepInlineTextNodes?: boolean }

  • مثال‌ها

    expect(".my_element").toHaveOuterHTML(`
        <div class="my_element">
            Some <strong>text</strong>
        </div>
    `);
    
toHaveProperty(property, value[, options])

انتظار دارد Target دریافت‌شده مقدار خاصیت داده‌شدهٔ خود را با value داده‌شده مطابق داشته باشد. اگر هیچ مقداری داده نشود: matcher در عوض بررسی می‌کند که خاصیت داده‌شده روی target وجود دارد.

  • پارامترها

    • property: string

    • value: any

    • options: { message?: string }

  • مثال‌ها

    expect("button").toHaveProperty("tabIndex", 0);
    expect("input").toHaveProperty("ontouchstart");
    expect("script").toHaveProperty("src", "./index.js");
    
toHaveRect(rect[, options])

انتظار دارد DOMRect مربوط به Target دریافت‌شده با شیء rect داده‌شده مطابقت داشته باشد. شیء rect می‌تواند یکی از موارد زیر باشد:

  • یک شیء DOMRect؛

  • یک رشتهٔ سلکتور CSS (برای دریافت rect تنها عنصر مطابق)؛

  • یک node.

اگر مقدار حاصل rect یک node باشد، rectهای هر دو node با هم مقایسه می‌شوند.

  • پارامترها

    • rect: Partial<DOMRect> | Target

    • options: { message?: string, trimPadding?: boolean }

  • مثال‌ها

    expect("button").toHaveRect({ x: 20, width: 100, height: 50 });
    expect("button").toHaveRect(".container");
    
toHaveStyle(style[, options])

انتظار دارد Target دریافت‌شده با ویژگی‌های style داده‌شده مطابقت داشته باشد.

  • پارامترها

    • style: string | Record<string, string | RegExp>

    • options: { message?: string }

  • مثال‌ها

    expect("button").toHaveStyle({ color: "red" });
    expect("p").toHaveStyle("text-align: center");
    
toHaveText(text[, options])

انتظار دارد محتوای text مربوط به Target دریافت‌شده یکی از موارد زیر باشد:

  • دقیقاً برابر با یک رشتهٔ داده‌شده باشد؛

  • با یک عبارت منظم داده‌شده مطابقت داشته باشد.

توجه: از innerHTML برای بازیابی محتوای متنی استفاده می‌شود تا visibility مربوط به CSS در نظر گرفته شود. این به این معنی نیز هست که مقادیر متنی از عناصر فرزند با استفاده از یک خط جدید به‌عنوان جداکننده به هم متصل می‌شوند.

  • پارامترها

    • text: string | RegExp

    • options: { message?: string, raw?: boolean }

  • مثال‌ها

    expect("p").toHaveText("lorem ipsum dolor sit amet");
    expect("header h1").toHaveText(/odoo/i);
    
toHaveValue(value[, options])

Expects the value of the received Target to either:

  • be strictly equal to a given string or number;

  • match a given regular expression;

  • contain file objects matching the given files list.

  • پارامترها

    • value: any

    • options: { message?: string }

  • مثال‌ها

    expect("input[type=email]").toHaveValue("john@doe.com");
    expect("input[type=file]").toHaveValue(new File(["foo"], "foo.txt"));
    expect("select[multiple]").toHaveValue(["foo", "bar"]);
    

Static methods

The expect helper function also contains static methods that can be used to run through a detached testing flow that isn't bound to one specific value at a certain moment.

These methods are mainly used to register steps or errors in the scope of the current test, and to evaluate them later on.

expect.assertions(expected)
نشانوندها
  • expected (number()) --

Expects the current test to have the expected amount of assertions. This number cannot be less than 1.

توجه

It is generally preferred to use expect.step() and expect.verifySteps() instead as it is more reliable and allows to test more extensively.

expect.errors(expected)
نشانوندها
  • expected (number()) --

Expects the current test to have the expected amount of errors.

This also means that from the moment this function is called, the test will accept that amount of errors before being considered as failed.

expect.step(value)
نشانوندها
  • value (unknown()) --

Registers a step for the current test, that can be consumed by expect.verifySteps(). Unconsumed steps will fail the test.

expect.verifyErrors(errors[, options])
نشانوندها
  • errors (unknown[]()) --

  • options ({ message?: string }()) --

بازگشت ها

boolean

Expects the received matchers to match the errors thrown since the start of the test or the last call to expect.verifyErrors(). Calling this matcher will reset the list of current errors.

expect.verifyErrors([/RPCError/, /Invalid domain AST/]);
expect.verifySteps(steps[, options])
نشانوندها
  • steps (unknown[]()) --

  • options ({ ignoreOrder?: boolean, message?: string, partial?: boolean }()) --

بازگشت ها

boolean

Expects the received steps to be equal to the steps emitted since the start of the test or the last call to expect.verifySteps(). Calling this matcher will reset the list of current steps.

expect.step("web_read_group");
expect.step([1, 2]);
expect.verifySteps(["web_read_group", [1, 2]]);
expect.waitForErrors(errors[, options])
نشانوندها
  • errors (unknown[]()) --

  • options ({ message?: string }()) --

بازگشت ها

Promise<boolean>

Same as expect.verifyErrors(), but will not immediatly fail if errors are not caught yet, and will instead wait for a certain timeout (default: 2000ms) to allow errors to be caught later.

Checks are performed initially, at the end of the timeout, and each time an error is detected.

fetch("invalid/url");
await expect.waitForErrors([/RPCError/]);
expect.waitForSteps(steps[, options])
نشانوندها
  • steps (unknown[]()) --

  • options ({ ignoreOrder?: boolean, message?: string, partial?: boolean }()) --

بازگشت ها

Promise<boolean>

Same as expect.verifySteps(), but will not immediatly fail if steps have not been registered yet, and will instead wait for a certain timeout (default: 2000ms) to allow steps to be registered later.

Checks are performed initially, at the end of the timeout, and each time a step is registered.

// ... step on each 'web_read_group' call
fetch(".../call_kw/web_read_group");
await expect.waitForSteps(["web_read_group"]);

DOM: queries

Custom DOM selectors

Here's a brief section on DOM selectors in Hoot, as they support additional pseudo-classes that can be used to target elements based on non-standard features, such as their text content or their global position in the document.

  • :contains(text)

    matches nodes whose text content matches the given text

    • given text supports regular expression syntax (e.g. :contains(/^foo.+/)) and is case-insensitive (unless using the i flag at the end of the regex)

  • :displayed

    matches nodes that are "displayed" (see isDisplayed)

  • :empty

    matches nodes that have an empty content (value or text content)

  • :eq(n)

    returns the nth node based on its global position (0-based index);

  • :first

    returns the first node matching the selector (in the whole document)

  • :focusable

    matches nodes that can be "focused" (see isFocusable)

  • :hidden

    matches nodes that are not "visible" (see isVisible)

  • :iframe

    matches nodes that are <iframe> elements, and returns their body if it is ready

  • :last

    returns the last node matching the selector (in the whole document)

  • :selected

    matches nodes that are selected (e.g. <option> elements)

  • :shadow

    matches nodes that have shadow roots, and returns their shadow root

  • :scrollable

    matches nodes that are scrollable (see isScrollable)

  • :value(text)

    matches nodes whose value matches the given text

    • given text supports regular expression syntax (e.g. :value(/^foo.+/)) and is case-insensitive (unless using the i flag at the end of the regex)

  • :visible

    matches nodes that are "visible" (see isVisible)

Query & node properties helpers

Hoot provides helpers to query nodes and some of their properties in a streamlined and elegant way. This can mainly be done through the use of queryX helpers:

queryAll(target[, options])

Returns a list of nodes matching the given Target. This function can either be used as a template literal tag (only supports string selector without options) or invoked the usual way.

The target can be:

  • a Node (or an iterable of nodes), or Window object;

  • a Document object (which will be converted to its body);

  • a string representing a custom selector (which will be queried from the root option).

An options object can be specified to filter 1 the results:

  • count: the exact number of nodes to match (throws an error if the number of nodes doesn't match);

  • displayed: whether the nodes must be "displayed" (see isDisplayed);

  • focusable: whether the nodes must be "focusable" (see isFocusable);

  • root: the root node to query the selector in (defaults to the current fixture);

  • visible: whether the nodes must be "visible" (see isVisible). * This option implies displayed

1

these filters (except for count and root) achieve the same result as using their homonym pseudo-classes on the final group of the given selector string, e.g.:

// These 2 will return the same result
queryAll`ul > li:visible`;
queryAll("ul > li", { visible: true });
بازگشت ها

Node[]

queryAllAttributes(target, attribute[, options])

Performs a queryAll() on the given target and returns a list of attribute values.

بازگشت ها

string[] list of attribute values

queryAllProperties(target, property[, options])

Performs a queryAll() on the given target and returns a list of property values.

بازگشت ها

unknown[] list of property values

queryAllTexts(target[, options])

Performs a queryAll() on the given target and returns a list of text contents.

بازگشت ها

string[] list of text contents

queryAllValues(target[, options])

Performs a queryAll() on the given target and returns a list of values.

بازگشت ها

string[] a list of values

queryAttribute(target, attribute[, options])

Performs a queryOne() with the given arguments and returns the value of the given attribute of the matching node.

بازگشت ها

string the attribute value

queryFirst(target[, options])

Performs a queryAll() with the given arguments and returns the first result or null.

بازگشت ها

Node | null the first matching node

queryOne(target[, options])

Performs a queryAll() with the given arguments, along with a forced count: 1 option to ensure only one node matches the given Target.

The returned value is a single node instead of a list of nodes.

بازگشت ها

Node a single node

queryText(target[, options])

Performs a queryOne() with the given arguments and returns the text of the matching node.

بازگشت ها

string text of the matching node

queryValue(target[, options])

Performs a queryOne() with the given arguments and returns the value of the matching node.

بازگشت ها

string value of the matching node

All of the above helpers are synchronous, meaning that they will attempt to query nodes instantly. Although some use cases require the element to be awaited for an arbitrary amount of time, unknown in advance due to UI fetching and rendering complexity.

Hoot provides 2 methods to wait for an element to appear / disappear within a certain time frame (by default: 200 milliseconds) for such cases:

waitFor(target[, options])

Combination of queryAll() and waitUntil(): waits for a given target to match elements in the DOM and returns the first matching node when it appears (or immediately if it is already present).

بازگشت ها

Promise<Node> containing the first matching node

waitForNone(target[, options])

Opposite of waitFor() waits for a given target to disappear from the DOM.

بازگشت ها

Promise<number> containing the number of matching nodes

DOM: interaction helpers

Along with querying elements, it is often required to interact with them. As such, Hoot provides helpers to simulate various user interactions on elements.

These can be split into 2 types based on their parameters: pointer-based interaction helpers, and the other ones.

Pointer interaction helpers:

Pointer interaction helpers (such as click() or drag()) will simulate actual pointer movements and events on the given target, and on any previous element the pointer was supposed to have been.

check(target[, options])

Ensures that the given Target is checked.

If it is not checked, a click() is simulated on the input. If the input is still not checked after the click, an error is thrown.

بازگشت ها

Promise<Event[]>

check("input[type=checkbox]"); // Checks the first <input> checkbox element
click(target[, options])

Performs a click sequence on the given Target.

The event sequence is as follows:

  • pointerdown

  • [desktop] mousedown

  • [touch] touchstart

  • [target is not active element] blur

  • [target is focusable] focus

  • pointerup

  • [desktop] mouseup

  • [touch] touchend

  • click

  • dblclick if click is not prevented & current click count is even

بازگشت ها

Promise<Event[]>

click("button"); // Clicks on the first <button> element
dblclick(target[, options])

Performs two click() sequences on the given Target.

بازگشت ها

Promise<Event[]>

dblclick("button"); // Double-clicks on the first <button> element
drag(target[, options])

Starts a drag sequence on the given Target.

Returns a set of helper functions to direct the sequence:

  • moveTo: moves the pointer to the given target;

  • drop: drops the dragged element on the given target (if any);

  • cancel: cancels the drag sequence.

بازگشت ها

Promise<DragHelpers>

drag(".card:first").drop(".card:last"); // Drags the first card onto the last one

drag(".card:first").moveTo(".card:last").drop(); // Same as above

const { cancel, moveTo } = await drag(".card:first"); // Starts the drag sequence
moveTo(".card:eq(3)"); // Moves the dragged card to the 4th card
cancel(); // Cancels the drag sequence
hover(target[, options])

Performs a hover sequence on the given Target.

The event sequence is as follows:

  • pointerover

  • [desktop] mouseover

  • pointerenter

  • [desktop] mouseenter

  • pointermove

  • [desktop] mousemove

  • [touch] touchmove

بازگشت ها

Promise<Event[]>

hover("button"); // Hovers the first <button> element
pointerDown(target[, options])

Performs a pointer down on the given Target.

The event sequence is as follows:

  • pointerdown

  • [desktop] mousedown

  • [touch] touchstart

  • [target is not active element] blur

  • [target is focusable] focus

بازگشت ها

Promise<Event[]>

pointerDown("button"); // Focuses to the first <button> element
pointerUp(target[, options])

Performs a pointer up on the given Target.

The event sequence is as follows:

  • pointerup

  • [desktop] mouseup

  • [touch] touchend

بازگشت ها

Promise<Event[]>

pointerUp("body"); // Triggers a pointer up on the <body> element
scroll(target, position[, options])

Performs a scroll event sequence on the given Target.

The event sequence is as follows:

  • [desktop] wheel

  • scroll

بازگشت ها

Promise<Event[]>

scroll("body", { y: 0 }); // Scrolls to the top of <body>
setInputRange(target, value[, options])

Sets the given value to the current "input[type=range]" Target.

The event sequence is as follows:

  • pointerdown

  • input

  • change

  • pointerup

بازگشت ها

Promise<Event[]>

uncheck(target[, options])

Ensures that the given Target is unchecked.

If it is checked, a click() is triggered on the input. If the input is still checked after the click, an error is thrown.

بازگشت ها

Promise<Event[]>

uncheck("input[type=checkbox]"); // Unchecks the first <input> checkbox element

Other interaction helpers:

Other interaction helpers will not have a target parameter. It is not needed, since pressing keys on a keyboard (for example) is done on the current active element.

clear([options])

Clears the value of the current active element.

This is done using the following sequence:

  • pressing "Control" & "A" to select the whole value;

  • pressing "Backspace" to delete the value;

  • (optional) triggering a "change" event by pressing "Enter".

بازگشت ها

Promise<Event[]>

clear(); // Clears the value of the current active element
edit(value[, options])

Combination of clear() and fill():

  • first, clears the input value (if any)

  • then fills the input with the given value

بازگشت ها

Promise<Event[]>

fill("foo"); // Types "foo" in the active element
edit("Hello World"); // Replaces "foo" by "Hello World"
fill(value[, options])

Fills the current active element with the given value. This helper is intended for <input> and <textarea> elements, with the exception of "checkbox" and "radio" types, which should be selected using the check helper.

If the target is an editable input, its string value will be input one character at a time, each generating its corresponding keyboard event sequence. This behavior can be overridden by passing the instantly option, which will instead simulate a control + v keyboard sequence, resulting in the whole text being pasted.

Note that the given value is appended to the current value of the element.

If the active element is a <input type="file"/>, the value should be a File/list of File object(s).

بازگشت ها

Promise<Event[]>

fill("Hello World"); // Types "Hello World" in the active element
fill("Hello World", { instantly: true }); // Pastes "Hello World" in the active element
fill(new File(["Hello World"], "hello.txt")); // Uploads a file named "hello.txt" with "Hello World" as content
keyDown(keyStrokes[, options])

Performs a key down sequence on the current active element.

The event sequence is as follows:

  • keydown

Additional actions will be performed depending on the key pressed:

  • Tab: focus next (or previous with shift) focusable element;

  • c: copy current selection to clipboard;

  • v: paste current clipboard content to current element;

  • Enter: submit the form if the target is a <button type="button"> or a <form> element, or trigger a change event on the target if it is an <input> element;

  • Space: trigger a click event on the target if it is an <input type="checkbox"> element.

بازگشت ها

Promise<Event[]>

keyDown(" "); // Space key
keyUp(keyStrokes[, options])

Performs a key up sequence on the current active element.

The event sequence is as follows:

  • keyup

بازگشت ها

Promise<Event[]>

keyUp("Enter");
leave([options])

Performs a leave sequence on the current Window.

The event sequence is as follows:

  • pointermove

  • [desktop] mousemove

  • [touch] touchmove

  • pointerout

  • [desktop] mouseout

  • pointerleave

  • [desktop] mouseleave

بازگشت ها

Promise<Event[]>

leave("button"); // Moves out of <button>
press(keyStrokes[, options])

Performs a keyboard event sequence on the current active element.

The event sequence is as follows:

  • keydown

  • keyup

بازگشت ها

Promise<Event[]>

pointerDown("button[type=submit]"); // Moves focus to <button>
keyDown("Enter"); // Submits the form

keyDown("Shift+Tab"); // Focuses previous focusable element

keyDown(["ctrl", "v"]); // Pastes current clipboard content
resize([dimensions[, options]])

Performs a resize event sequence on the current Window.

The event sequence is as follows:

  • resize

The target will be resized to the given dimensions, enforced by !important style attributes.

بازگشت ها

Promise<Event[]>

resize("body", { width: 1000, height: 500 }); // Resizes <body> to 1000x500
select(value[, options])

Performs a selection event sequence on the current active element. This helper is intended for <select> elements only.

The event sequence is as follows:

  • change

بازگشت ها

Promise<Event[]>

click("select[name=country]"); // Focuses <select> element
select("belgium"); // Selects the <option value="belgium"> element
setInputFiles(files[, options])

Gives the given File list to the current file input. This helper only works if a file input has been previously interacted with (by clicking on it).

بازگشت ها

Promise<Event[]>

unload([options])

Triggers a "beforeunload" event on the current Window.

بازگشت ها

Promise<Event[]>

Mockها

By default, a lot of low-level features are mocked by Hoot: clipboard, fetch, localStorage, etc. These mocks are intended to not produce any side-effect that would disturb the test runner or the context of other tests, while still providing the same interface to allow tests to rely on these features seamlessly.

There is also a need (most of the time) to force actions on these features or change their behavior for a test, so there exist helpers to interact with these mocked features. The following sections will list the main mocked features and the means to interact with them.

Time

Most asynchronous features are mocked: "timers" (setTimeout, setInterval and requestAnimationFrame), Date and performance all behave normally, but can be canceled or sped-up manually to considerably shorten the actual duration of tests. For example: all "timers" are canceled at the end of each test to avoid side-effects for the next one.

مهم

There are 2 main timing behaviors that are NOT mocked:

  • Promise objects and related API;

  • OWL's timer functions: to wait for OWL rendering functions, you'll have to resort to the animationFrame helper.

Network

In general, we don't want to perform actual network calls in tests. To ensure this, all calls to fetch and XMLHttpRequest have been re-routed to a function given to mockFetch().

توجه

In Odoo, this is generally implicitly handled by a MockServer which is spawned by the mock environment, i.e. any time a component is rendered using the mountWithCleanup helper.

Related helpers

mockFetch([fetchFn])

Mocks the fetch function by replacing it with a given fetchFn.

The return value of fetchFn is used as the response of the mocked fetch, or wrapped in a MockResponse object if it does not meet the required format.

mockFetch((input, init) => {
    if (input === "/../web_search_read") {
        return { records: [{ id: 3, name: "john" }] };
    }
    // ...
});
mockFetch((input, init) => {
    if (input === "/translations") {
        const translations = {
            "Hello, world!": "Bonjour, monde !",
            // ...
        };
        return new Response(JSON.stringify(translations));
    }
});
mockWebSocket([onWebSocketConnected])

Activates mock WebSocket classe:

  • websocket connections will be handled by window.fetch (see mockFetch());

  • the onWebSocketConnected callback will be called after a websocket has been created.

mockWorker([onWorkerConnected])

Activates mock Worker and SharedWorker classes:

  • actual code fetched by worker URLs will then be handled by window.fetch (see mockFetch());

  • the onWorkerConnected callback will be called after a worker has been created.

Notable global features

The following features may not have any specific mocked feature added, but they do work as expected without changing the actual properties they were meant to:

  • Document

    Both title and cookie can be set and read without changing the actual properties of the current document.

  • History

    The history API is mocked and bound to the mockLocation object to return the same values and provide consistency.

  • Location

    Hoot returns a mockLocation object to use instead of window.location, but this relies on the use of an indirection in the actual production code.

    مهم

    This feature will only work if an indirection is set between production code and calls to window.location. In Odoo, it works because the @web/core/browser module provides such an indirection, and that module is mocked in test environments to redirect to the mockLocation object.

  • Navigator

    Most used navigator features, such as the clipboard API and userAgent, have been mocked to hijack their actual behaviors. Its permissions object has been bound to a global mock of the permissions API.

  • Notification

    Notifications have been mocked, with the "notification" permissions bound to the global mocked permissions API.

  • Permissions

    Permissions can enable or disable other APIs by being given the "granted" or "denied" statuses. This can be done through the mockPermission helper.

  • Storage

    localStorage and sessionStorage both point to "virtual" storages.

  • Touch

    Touch features can be force-activated or deactivated globally for a given test/suite using the mockTouch() helper. It will mock both the presence of touch handlers like ontouchstart on window, as well as the "pointer" media being set to fine or coarse.

Related helpers

mockPermission(name[, value])

Sets the given value for the given permission. This allows to enable or prevent certain APIs (see the Permissions API).

// Prevents the whole notification API from working
mockPermission("notifications", "denied");
mockTouch(setTouch)

Toggles touch features on or off in the current Window.