Remove obsolete tests and versioning files for odex30_web

- Deleted home_menu.test.js and webclient_tests.js as they are no longer needed.
- Removed the test initialization file and the associated test_odex.py file.
- Eliminated version.py to clean up the versioning process.
This commit is contained in:
Altahir Hassan 2026-01-06 10:13:41 +04:00
parent 049eed2495
commit bc4a5b32dd
27 changed files with 2 additions and 3719 deletions

View File

@ -1,457 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * odex30_web
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-01 22:17+0000\n"
"PO-Revision-Date: 2026-01-01 22:17+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.js:0
msgid "%s days"
msgstr "%s أيام"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/settings_form_view/res_config_edition.xml:0
msgid "(Enterprise Edition)"
msgstr "(النسخة الشركاتية)"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.js:0
msgid "1 month"
msgstr "شهر واحد"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/views/list/list_renderer_desktop.xml:0
msgid "Add Custom Field"
msgstr "إضافة حقل مخصص"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/views/kanban/kanban_header_patch.js:0
msgid "Automations"
msgstr "الأتمتة"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/promote_studio_dialog/promote_studio_dialog.xml:0
msgid "Build new apps from scratch"
msgstr "بناء تطبيقات جديدة من الصفر"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/promote_studio_dialog/promote_studio_dialog.xml:0
msgid "Build new reports"
msgstr "بناء تقارير جديدة"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/share_url/burger_menu.xml:0
msgid "Close menu"
msgstr "إغلاق القائمة"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid ""
"Contact your sales representative to help you to unlink your previous "
"database"
msgstr ""
"تواصل مع مندوب المبيعات لمساعدتك في فصل قاعدة البيانات السابقة"
""
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/promote_studio_dialog/promote_studio_dialog.xml:0
msgid "Create automation rules"
msgstr "إنشاء قواعد الأتمتة"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/promote_studio_dialog/promote_studio_dialog.xml:0
msgid "Customize Reports"
msgstr "تخصيص التقارير"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/promote_studio_dialog/promote_studio_dialog.xml:0
msgid "Customize any screen"
msgstr "تخصيص أي شاشة"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/color_scheme/color_scheme_menu_items.js:0
msgid "Dark Mode"
msgstr "الوضع الداكن"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/settings_form_view/res_config_edition.xml:0
msgid "Database expiration:"
msgstr "انتهاء صلاحية قاعدة البيانات:"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/promote_studio_dialog/promote_studio_dialog.xml:0
msgid "Define webhooks"
msgstr "تعريف Webhooks"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/promote_studio_dialog/promote_studio_dialog.xml:0
msgid "Discard"
msgstr "إلغاء"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "Dismiss"
msgstr "إغلاق"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "Error reason:"
msgstr "سبب الخطأ:"
#. module: odex30_web
#: model:ir.model,name:odex30_web.model_ir_http
msgid "HTTP Routing"
msgstr "مسار HTTP"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/home_menu_service.js:0
msgid "Home"
msgstr "الرئيسية"
#. module: odex30_web
#: model:ir.model.fields,field_description:odex30_web.field_res_users_settings__homemenu_config
msgid "Home Menu Configuration"
msgstr "إعدادات قائمة الرئيسية"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/navbar/navbar.js:0
msgid "Home menu"
msgstr "قائمة الرئيسية"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "I paid, please recheck!"
msgstr "لقد دفعت، يرجى إعادة التحقق!"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/promote_studio_dialog/promote_studio_dialog.xml:0
msgid "Install Odoo Studio and its dependencies"
msgstr "تثبيت Odoo Studio وتبعياته"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/promote_studio_dialog/promote_studio_dialog.xml:0
msgid "Learn More"
msgstr "تعرف على المزيد"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "Log in as an administrator to correct the issue."
msgstr "سجل الدخول كمدير لإصلاح المشكلة."
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/home_menu.xml:0
msgid "No result"
msgstr "لا توجد نتائج"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/settings_form_view/res_config_edition.xml:0
msgid "Odoo"
msgstr "أودو"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/settings_form_view/res_config_edition.xml:0
msgid "Odoo Enterprise Edition License V1.0"
msgstr "رخصة Odoo Enterprise Edition V1.0"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/views/list/list_renderer_desktop.js:0
msgid "Odoo Studio - Add new fields to any view"
msgstr "Odoo Studio - إضافة حقول جديدة إلى أي عرض"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/views/kanban/kanban_header_patch.js:0
msgid "Odoo Studio - Customize workflows in minutes"
msgstr "Odoo Studio - تخصيص سير العمل في دقائق"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "Odoo Support"
msgstr "دعم أودو"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "Paste code here"
msgstr "الصق الكود هنا"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/navbar/navbar.js:0
msgid "Previous view"
msgstr "العرض السابق"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.js:0
msgid "Register"
msgstr "تسجيل"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "Register your subscription"
msgstr "تسجيل اشتراكك"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "Renew now"
msgstr "تجديد الآن"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.js:0
msgid "Retry"
msgstr "إعادة المحاولة"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "Send an email"
msgstr "إرسال بريد إلكتروني"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "Sending the instructions by email ..."
msgstr "إرسال التعليمات بالبريد الإلكتروني..."
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/share_url/share_url.js:0
msgid "Share"
msgstr "مشاركة"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/share_url/burger_menu.xml:0
msgid "Share URL"
msgstr "مشاركة الرابط"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid ""
"Something went wrong while registering your database. You can try again or "
"contact"
msgstr ""
"حدث خطأ أثناء تسجيل قاعدة البيانات. يمكنك المحاولة مرة أخرى أو التواصل"
" مع"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/promote_studio_dialog/promote_studio_dialog.xml:0
msgid "Start using Odoo Studio"
msgstr "ابدأ استخدام Odoo Studio"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "Subscription Code:"
msgstr "كود الاشتراك:"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/home_menu.xml:0
msgid "TIP"
msgstr "نصيحة"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid ""
"Thank you, your registration was successful! Your database is valid until"
msgstr ""
"شكراً، تم تسجيلك بنجاح! قاعدة بياناتك صالحة حتى"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/enterprise_subscription_service.js:0
msgid ""
"Thank you, your registration was successful! Your database is valid until "
"%s."
msgstr ""
"شكراً، تم تسجيلك بنجاح! قاعدة بياناتك صالحة حتى %s."
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid ""
"The instructions to unlink your subscription from the previous database(s) "
"have been sent"
msgstr ""
"تم إرسال التعليمات لفصل اشتراكك من قواعد البيانات السابقة"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.js:0
msgid "This database has expired. "
msgstr "انتهت صلاحية هذه قاعدة البيانات. "
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.js:0
msgid "This database will expire in %s. "
msgstr "ستنتهي صلاحية قاعدة البيانات هذه في %s. "
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.js:0
msgid "This demo database will expire in %s. "
msgstr "ستنتهي صلاحية قاعدة بيانات العرض التجريبي في %s. "
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "Unable to send the instructions by email, please contact the"
msgstr "تعذر إرسال التعليمات بالبريد الإلكتروني، يرجى التواصل مع"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/promote_studio_dialog/promote_studio_dialog.xml:0
msgid "Unleash the power of Odoo Studio:"
msgstr "استخدم قوة Odoo Studio:"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "Upgrade your subscription"
msgstr "ترقية اشتراكك"
#. module: odex30_web
#: model:ir.model,name:odex30_web.model_res_users_settings
msgid "User Settings"
msgstr "إعدادات المستخدم"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/promote_studio_dialog/promote_studio_dialog.xml:0
msgid "Want to tailor-make your Odoo?"
msgstr "هل تريد تخصيص أودو حسب احتياجاتك؟"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid ""
"You have more users or more apps installed than your subscription allows."
msgstr ""
"لديك مستخدمون أو تطبيقات مثبتة أكثر مما يسمح به اشتراكك."
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid ""
"You will be able to register your database once you have installed your "
"first app."
msgstr ""
"يمكنك تسجيل قاعدة البيانات بعد تثبيت أول تطبيق."
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "Your subscription code"
msgstr "كود اشتراكك"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.js:0
msgid ""
"Your subscription expired %s days ago. This database will be blocked soon. "
msgstr ""
"انتهى اشتراكك منذ %s أيام. سيتم حظر قاعدة البيانات قريباً. "
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.js:0
msgid "Your subscription expires in %s days. "
msgstr "ينتهي اشتراكك بعد %s أيام. "
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "Your subscription is already linked to a database."
msgstr "اشتراكك مرتبط بقاعدة بيانات بالفعل."
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "Your subscription was updated and is valid until"
msgstr "تم تحديث اشتراكك وهو صالح حتى"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/promote_studio_dialog/promote_studio_dialog.xml:0
msgid "and more!"
msgstr "وأكثر!"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "buy a subscription"
msgstr "شراء اشتراك"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "buy a subscription."
msgstr "شراء اشتراك."
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "or"
msgstr "أو"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/expiration_panel.xml:0
msgid "to the subscription owner to confirm the change, enter a new code or"
msgstr ""
"لصاحب الاشتراك لتأكيد التغيير، أدخل كود جديد أو"
#. module: odex30_web
#. odoo-javascript
#: code:addons/odex30_web/static/src/webclient/home_menu/home_menu.xml:0
msgid "— open me anywhere with"
msgstr ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

View File

@ -1,35 +0,0 @@
<svg width="1920" height="1080" viewBox="0 0 1920 1080" xmlns="http://www.w3.org/2000/svg">
<path d="M3.51001 1080H76.35L1153.55 0H3.51001V1080Z" fill="url(#o_app_switcher_gradient_01)"/>
<path d="M76.35 1080H842.98L1920 0.18V0H1153.55L76.35 1080Z" fill="url(#o_app_switcher_gradient_02)"/>
<path d="M1920 0.180176L842.98 1080H1063.11L1920 220.88V0.180176Z" fill="url(#o_app_switcher_gradient_03)"/>
<path d="M1920 1080V220.88L1063.11 1080H1920Z" fill="url(#o_app_switcher_gradient_04)"/>
<rect width="1920" height="1080" fill="url(#o_app_switcher_gradient_05)" fill-opacity="0.25"/>
<rect width="1920" height="1080" fill="#E9E6F9" fill-opacity="0.25"/>
<defs>
<linearGradient id="o_app_switcher_gradient_01" x1="-222.43" y1="727.19" x2="904.26" y2="-76.67" gradientUnits="userSpaceOnUse">
<stop offset="0.1" stop-color="white"/>
<stop offset="0.36" stop-color="#FEFEFE"/>
<stop offset="0.68" stop-color="#EAE7F9"/>
<stop offset="1" stop-color="#E4E9F7"/>
</linearGradient>
<linearGradient id="o_app_switcher_gradient_02" x1="407.23" y1="1021.82" x2="1848.47" y2="-153.08" gradientUnits="userSpaceOnUse">
<stop offset="0.32" stop-color="#FEFEFE"/>
<stop offset="0.66" stop-color="#EAE7F9"/>
<stop offset="1" stop-color="#E5E2F6"/>
</linearGradient>
<linearGradient id="o_app_switcher_gradient_03" x1="1142.33" y1="846.57" x2="1951.83" y2="136.16" gradientUnits="userSpaceOnUse">
<stop offset="0.15" stop-color="white"/>
<stop offset="0.51" stop-color="#F7F0FD"/>
<stop offset="0.85" stop-color="#F0E7F9"/>
</linearGradient>
<linearGradient id="o_app_switcher_gradient_04" x1="1409.74" y1="1071" x2="2070.98" y2="526.01" gradientUnits="userSpaceOnUse">
<stop offset="0.45" stop-color="white"/>
<stop offset="0.88" stop-color="#F7F0FD"/>
<stop offset="1" stop-color="#ECE5F8"/>
</linearGradient>
<radialGradient id="o_app_switcher_gradient_05" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(960 540) rotate(90) scale(540 960)">
<stop stop-color="#9996A9" stop-opacity="0.53"/>
<stop offset="1" stop-color="#7A768F"/>
</radialGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,9 +0,0 @@
// = Search Panel
// ============================================================================
// No CSS hacks, variables overrides only
.o_search_panel_section {
.o_popover > & .list-group {
--#{$prefix}list-group-active-bg: #{$o-gray-400};
}
}

View File

@ -1,195 +0,0 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { session } from "@web/session";
import { browser } from "@web/core/browser/browser";
import { deserializeDateTime, serializeDate, formatDate } from "@web/core/l10n/dates";
import { useService } from "@web/core/utils/hooks";
import { _t } from "@web/core/l10n/translation";
import { ExpirationPanel } from "./expiration_panel";
import { cookie } from "@web/core/browser/cookie";
import { rpc } from "@web/core/network/rpc";
const { DateTime } = luxon;
import { Component, xml, useState } from "@odoo/owl";
function daysUntil(datetime) {
const duration = datetime.diff(DateTime.utc(), "days");
return Math.round(duration.values.days);
}
export class SubscriptionManager {
constructor(env, { orm, notification }) {
this.env = env;
this.orm = orm;
this.notification = notification;
if (session.expiration_date) {
this.expirationDate = deserializeDateTime(session.expiration_date);
} else {
// If no date found, assume 1 month and hope for the best
this.expirationDate = DateTime.utc().plus({ days: 30 });
}
this.expirationReason = session.expiration_reason;
// Hack: we need to know if there is at least one app installed (except from App and
// Settings). We use mail to do that, as it is a dependency of almost every addon. To
// determine whether mail is installed or not, we check for the presence of the key
// "storeData" in session_info, as it is added in mail.
this.hasInstalledApps = "storeData" in session;
// "user" or "admin"
this.warningType = session.warning;
this.lastRequestStatus = null;
this.isWarningHidden = cookie.get("oe_instance_hide_panel");
}
get formattedExpirationDate() {
return formatDate(this.expirationDate, { format: "DDD" });
}
get daysLeft() {
return daysUntil(this.expirationDate);
}
get unregistered() {
return ["trial", "demo", false].includes(this.expirationReason);
}
hideWarning() {
// Hide warning for 24 hours.
cookie.set("oe_instance_hide_panel", true, 24 * 60 * 60);
this.isWarningHidden = true;
}
async buy() {
const limitDate = serializeDate(DateTime.utc().minus({ days: 15 }));
const args = [
[
["share", "=", false],
["login_date", ">=", limitDate],
],
];
const nbUsers = await this.orm.call("res.users", "search_count", args);
browser.location = `https://www.odoo.com/odoo-enterprise/upgrade?num_users=${nbUsers}`;
}
async submitCode(OdexCode) {
const [oldDate, ] = await Promise.all([
this.orm.call("ir.config_parameter", "get_param", ["database.expiration_date"]),
this.orm.call("ir.config_parameter", "set_param", [
"database.enterprise_code",
OdexCode,
])
]);
await this.orm.call("publisher_warranty.contract", "update_notification", [[]]);
const [linkedSubscriptionUrl, linkedEmail, expirationDate] = await Promise.all([
this.orm.call("ir.config_parameter", "get_param", [
"database.already_linked_subscription_url",
]),
this.orm.call("ir.config_parameter", "get_param", ["database.already_linked_email"]),
this.orm.call("ir.config_parameter", "get_param", [
"database.expiration_date",
])
]);
if (linkedSubscriptionUrl) {
this.lastRequestStatus = "link";
this.linkedSubscriptionUrl = linkedSubscriptionUrl;
this.mailDeliveryStatus = null;
this.linkedEmail = linkedEmail;
} else if (expirationDate !== oldDate) {
this.lastRequestStatus = "success";
this.expirationDate = deserializeDateTime(expirationDate);
if (this.daysLeft > 30) {
this.notification.add(
_t(
"Thank you, your registration was successful! Your database is valid until %s.",
this.formattedExpirationDate
),
{ type: "success" }
);
}
} else {
this.lastRequestStatus = "error";
}
}
async checkStatus() {
await this.orm.call("publisher_warranty.contract", "update_notification", [[]]);
const expirationDateStr = await this.orm.call("ir.config_parameter", "get_param", [
"database.expiration_date",
]);
this.lastRequestStatus = "update";
this.expirationDate = deserializeDateTime(expirationDateStr);
}
async sendUnlinkEmail() {
const sendUnlinkInstructionsUrl = await this.orm.call("ir.config_parameter", "get_param", [
"database.already_linked_send_mail_url",
]);
this.mailDeliveryStatus = "ongoing";
const { result, reason } = await rpc(sendUnlinkInstructionsUrl);
if (result) {
this.mailDeliveryStatus = "success";
} else {
this.mailDeliveryStatus = "fail";
this.mailDeliveryStatusError = reason;
}
}
async renew() {
const OdexCode = await this.orm.call("ir.config_parameter", "get_param", [
"database.enterprise_code",
]);
const url = "https://www.odoo.com/odoo-enterprise/renew";
const contractQueryString = OdexCode ? `?contract=${OdexCode}` : "";
browser.location = `${url}${contractQueryString}`;
}
async upsell() {
const limitDate = serializeDate(DateTime.utc().minus({ days: 15 }));
const [OdexCode, nbUsers] = await Promise.all([
this.orm.call("ir.config_parameter", "get_param", ["database.enterprise_code"]),
this.orm.call("res.users", "search_count", [
[
["share", "=", false],
["login_date", ">=", limitDate],
],
]),
]);
const url = "https://www.odoo.com/odoo-enterprise/upsell";
const contractQueryString = OdexCode ? `&contract=${OdexCode}` : "";
browser.location = `${url}?num_users=${nbUsers}${contractQueryString}`;
}
}
class ExpiredSubscriptionBlockUI extends Component {
static props = {};
// TODO the "o_blockUI" div in there seems useless (it has 0 height and thus displays and does nothing)
static template = xml`
<t t-if="subscription.daysLeft &lt;= 0">
<div class="o_blockUI"/>
<div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 1100" class="d-flex align-items-center justify-content-center">
<ExpirationPanel/>
</div>
</t>`;
static components = { ExpirationPanel };
setup() {
this.subscription = useState(useService("enterprise_subscription"));
}
}
export const enterpriseSubscriptionService = {
name: "enterprise_subscription",
dependencies: ["orm", "notification"],
start(env, { orm, notification }) {
registry
.category("main_components")
.add("expired_subscription_block_ui", { Component: ExpiredSubscriptionBlockUI });
return new SubscriptionManager(env, { orm, notification });
},
};
registry.category("services").add("enterprise_subscription", enterpriseSubscriptionService);

View File

@ -1,97 +0,0 @@
/** @odoo-module **/
import { useService } from "@web/core/utils/hooks";
import { Transition } from "@web/core/transition";
import { _t } from "@web/core/l10n/translation";
import { Component, useState, useRef } from "@odoo/owl";
const { DateTime } = luxon;
/**
* Expiration panel
*
* Component representing the banner located on top of the home menu. Its purpose
* is to display the expiration state of the current database and to help the
* user to buy/renew its subscription.
* @extends Component
*/
export class ExpirationPanel extends Component {
static template = "DatabaseExpirationPanel";
static props = {};
static components = { Transition };
setup() {
this.subscription = useState(useService("enterprise_subscription"));
this.state = useState({
displayRegisterForm: false,
});
this.inputRef = useRef("input");
}
get buttonText() {
return this.subscription.lastRequestStatus === "error" ? _t("Retry") : _t("Register");
}
get alertType() {
if (this.subscription.lastRequestStatus === "success") {
return "success";
}
const { daysLeft } = this.subscription;
if (daysLeft <= 6) {
return "danger";
} else if (daysLeft <= 16) {
return "warning";
}
return "info";
}
get expirationMessage() {
const { daysLeft } = this.subscription;
if (daysLeft <= 0) {
return _t("This database has expired. ");
}
const delay = daysLeft === 30 ? _t("1 month") : _t("%s days", daysLeft);
if (this.subscription.expirationReason === "demo") {
return _t("This demo database will expire in %s. ", delay);
}
const expirationDate = this.subscription.expirationDate;
const today = DateTime.now();
const diff = expirationDate.diff(today);
if (this.subscription.expirationReason !== 'renewal') {
return _t("This database will expire in %s. ", delay);
} else {
if (daysLeft > 15) {
return _t(
"Your subscription expires in %s days. ",
daysLeft - 15
);
} else {
return _t(
"Your subscription expired %s days ago. This database will be blocked soon. ",
(diff.as("days") | 0)
);
}
}
}
showRegistrationForm() {
this.state.displayRegisterForm = !this.state.displayRegisterForm;
}
async onCodeSubmit() {
const OdexCode = this.inputRef.el.value;
if (!OdexCode) {
return;
}
await this.subscription.submitCode(OdexCode);
if (this.subscription.lastRequestStatus === "success") {
this.state.displayRegisterForm = false;
} else {
this.state.buttonText = _t("Retry");
}
}
}

View File

@ -1,8 +0,0 @@
.database_expiration_panel .oe_instance_register_form {
max-height: 0;
transition: max-height 0.4s;
&.o-vertical-slide-enter-active {
max-height: 10rem; // fixed value is required to properly trigger transition
}
}

View File

@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="DatabaseExpirationPanel">
<div role="alert"
class="database_expiration_panel alert o-hidden-ios text-center mt-4"
t-attf-class="alert-{{alertType}}"
>
<!-- t-translation="off" should be on next a element below -->
<a t-if="subscription.daysLeft &gt; 0" href="#" class="oe_instance_hide_panel float-end alert-link" t-on-click.prevent="() => subscription.hideWarning()" aria-label="Dismiss">×</a>
<span t-if="!subscription.lastRequestStatus" class="oe_instance_register">
<t t-if="!subscription.hasInstalledApps">You will be able to register your database once you have installed your first app.</t>
<t t-else="">
<t t-esc="expirationMessage"/>
<t t-if="subscription.warningType === 'admin'">
<t t-if="subscription.unregistered">
<a class="oe_instance_register_show alert-link text-decoration-underline" href="#" t-on-click.prevent="showRegistrationForm">Register your subscription</a>
or
<a class="oe_instance_buy alert-link text-decoration-underline" href="#" t-on-click.prevent="() => subscription.buy()">buy a subscription</a>.
</t>
<t t-if="subscription.expirationReason === 'renewal'">
<div class="d-flex flex-wrap justify-content-center mt-2">
<a class="oe_instance_renew btn btn-link" href="#" t-on-click.prevent="() => subscription.renew()">Renew now </a>
<a class="check_enterprise_status btn btn-link" href="#" t-on-click.prevent="() => subscription.checkStatus()">I paid, please recheck!</a>
</div>
</t>
<t t-elif="subscription.expirationReason === 'upsell'">You have more users or more apps installed than your subscription allows.<br/>
<div class="d-flex flex-wrap justify-content-center mt-2">
<a class="oe_instance_upsell btn btn-link" href="#" t-on-click.prevent="() => subscription.upsell()">Upgrade your subscription </a>
<a class="check_enterprise_status btn btn-link" href="#" t-on-click.prevent="() => subscription.checkStatus()">I paid, please recheck!</a>
</div>
</t>
</t>
<t t-elif="subscription.warningType === 'user'">Log in as an administrator to correct the issue.</t>
</t>
</span>
<span t-if="subscription.lastRequestStatus === 'success'" class="oe_instance_register oe_instance_success">Thank you, your registration was successful! Your database is valid until <span><t t-esc="subscription.formattedExpirationDate"/></span>.</span>
<span t-elif="subscription.lastRequestStatus === 'update'" class="oe_instance_register oe_subscription_updated">Your subscription was updated and is valid until <span><t t-esc="subscription.formattedExpirationDate"/></span>.</span>
<span t-elif="subscription.lastRequestStatus === 'error'" class="oe_instance_register oe_instance_error">Something went wrong while registering your database. You can try again or contact <a class="alert-link text-decoration-underline" href="https://www.odoo.com/help" target="_blank">Odoo Support</a>.</span>
<span t-elif="subscription.lastRequestStatus === 'link'" class="oe_instance_register oe_database_already_linked">
Your subscription is already linked to a database.<br/>
<span t-if="subscription.linkedEmail" class="oe_contract_email_block">
<a href="#" class="oe_contract_send_mail alert-link text-decoration-underline" t-on-click.prevent="() => subscription.sendUnlinkEmail()">Send an email</a> to the subscription owner to confirm the change, enter a new code or <a class="oe_instance_buy" href="#" t-on-click.prevent="() => subscription.buy()">buy a subscription.</a>
<p t-if="subscription.mailDeliveryStatus === 'ongoing'">Sending the instructions by email ...</p>
<p t-elif="subscription.mailDeliveryStatus === 'success'">The instructions to unlink your subscription from the previous database(s) have been sent</p>
<p t-elif="subscription.mailDeliveryStatus === 'fail'">Unable to send the instructions by email, please contact the <a href="https://www.odoo.com/help" target="_blank">Odoo Support</a><br/>
Error reason: <t t-esc="subscription.mailDeliveryStatusError"/>
</p>
</span>
<span t-else="">Contact your sales representative to help you to unlink your previous database</span>
</span>
<Transition visible="state.displayRegisterForm and subscription.lastRequestStatus !== 'success'" t-slot-scope="transition" leaveDuration="400" name="'o-vertical-slide'">
<form class="oe_instance_register oe_instance_register_form d-flex flex-wrap align-items-center overflow-hidden justify-content-center mt-4" t-att-class="transition.className">
<label for="enterprise_code">Subscription Code: </label>
<input type="text" class="o_input w-auto mx-2 mb-2 mb-sm-0" t-ref="input"
placeholder="Paste code here"
title="Your subscription code"
/>
<button class="btn btn-primary" t-on-click.prevent="onCodeSubmit"><t t-esc="buttonText"/></button>
</form>
</Transition>
</div>
</t>
</templates>

View File

@ -4,5 +4,5 @@
.o_home_menu_background {
--homeMenu-bg-color: #000511;
--homeMenu-bg-image: url("/odex30_web/static/img/background-dark.jpg");
--homeMenu-bg-image: url("/odex30_web/static/img/background-dark.png");
}

View File

@ -4,6 +4,6 @@
size: cover;
attachment: fixed;
color: var(--homeMenu-bg-color, #{$o-gray-200});
image: var(--homeMenu-bg-image, url("/odex30_web/static/img/background-light.svg"));
image: var(--homeMenu-bg-image, url("/odex30_web/static/img/background-light.png"));
}
}

View File

@ -1,9 +0,0 @@
/** @odoo-module */
import { createWebClient } from "@web/../tests/webclient/helpers";
import { WebClientOdex } from "@odex30_web/webclient/webclient";
export function createOdexWebClient(params) {
params.WebClientClass = WebClientOdex;
return createWebClient(params);
}

View File

@ -1,107 +0,0 @@
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { click, queryAll } from "@odoo/hoot-dom";
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
import { defineActions, defineMenus, mountWithCleanup } from "@web/../tests/web_test_helpers";
import { Component, onMounted, xml } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { WebClientOdex } from "@odex30_web/webclient/webclient";
const actionRegistry = registry.category("actions");
const queryAllRoot = (selector) => queryAll(selector, { root: document.body });
class TestClientAction extends Component {
static template = xml`
<div class="test_client_action">
ClientAction_<t t-esc="props.action.params?.description"/>
</div>`;
static props = ["*"];
setup() {
onMounted(() => this.env.config.setDisplayName(`Client action ${this.props.action.id}`));
}
}
describe.current.tags("mobile");
beforeEach(() => {
defineMenus([
{
id: 1,
name: "App1",
appID: 1,
actionID: 1001,
xmlid: "menu_1",
},
]);
});
test("Burger Menu on home menu", async () => {
expect.assertions(5);
await mountWithCleanup(WebClientOdex);
await animationFrame();
expect(queryAllRoot(".o_burger_menu")).toHaveCount(0);
expect(queryAllRoot(".o_home_menu")).toBeVisible();
await click(queryAllRoot(".o_mobile_menu_toggle"));
await runAllTimers();
await animationFrame();
expect(queryAllRoot(".o_burger_menu")).toHaveCount(1);
expect(queryAllRoot(".o_user_menu_mobile")).toHaveCount(1);
await click(queryAllRoot(".o_sidebar_close"));
await animationFrame();
expect(".o_burger_menu").toHaveCount(0);
});
test("Burger Menu on home menu over an App", async () => {
expect.assertions(5);
actionRegistry.add("__test__client__action__", TestClientAction);
defineMenus([
{
id: 1,
children: [
{
id: 99,
name: "SubMenu",
appID: 1,
actionID: 1002,
webIconData: undefined,
webIcon: false,
},
],
},
]);
defineActions([
{
id: 1001,
tag: "__test__client__action__",
target: "main",
type: "ir.actions.client",
params: { description: "Id 1" },
},
]);
await mountWithCleanup(WebClientOdex);
await animationFrame();
await click(queryAllRoot(".o_draggable:first-of-type .o_app"));
await animationFrame();
await click(queryAllRoot(".o_menu_toggle"));
await animationFrame();
await click(queryAllRoot(".o_sidebar_topbar a.btn-primary"));
await animationFrame();
expect(queryAllRoot(".o_burger_menu")).toHaveCount(0);
expect(queryAllRoot(".o_home_menu")).toBeVisible();
await click(queryAllRoot(".o_mobile_menu_toggle"));
await animationFrame();
expect(queryAllRoot(".o_burger_menu")).toHaveCount(1);
expect(queryAllRoot(".o_burger_menu nav.o_burger_menu_content li")).toHaveCount(0);
expect(queryAllRoot(".o_burger_menu_content")).not.toHaveClass("o_burger_menu_dark");
});

View File

@ -1,72 +0,0 @@
import { describe, expect, test } from "@odoo/hoot";
import { click } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
class Partner extends models.Model {
foo = fields.Integer({ aggregator: "sum" });
_records = [
{
id: 1,
foo: 12,
},
{
id: 2,
foo: 1,
},
{
id: 3,
foo: 17,
},
{
id: 4,
foo: 2,
},
];
}
defineModels([Partner]);
describe.current.tags("mobile");
test("simple pivot rendering", async () => {
expect.assertions(2);
await mountView({
type: "pivot",
resModel: "partner",
arch: /* xml */ `
<pivot string="Partners">
<field name="foo" type="measure"/>
</pivot>
`,
});
expect(".o_pivot_view").toHaveClass("o_view_controller");
expect("td.o_pivot_cell_value:contains(32)").toHaveCount(1, {
message: "should contain a pivot cell with the sum of all records",
});
});
test("unselecting all measures should not crash pivot rendering", async () => {
expect.assertions(1);
await mountView({
type: "pivot",
resModel: "partner",
arch: /* xml */ `
<pivot string="Partners">
<field name="foo" type="measure"/>
</pivot>
`,
});
await click(".dropdown-toggle.btn.btn-primary:eq(1)");
await animationFrame();
await click(".dropdown-item.o_menu_item.selected:eq(0)");
await animationFrame();
expect("div.o_nocontent_help").toHaveCount(1, {
message: "Instead of error action helper will appear",
});
});

View File

@ -1,112 +0,0 @@
import { describe, expect, test } from "@odoo/hoot";
import { click, queryFirst } from "@odoo/hoot-dom";
import { animationFrame, mockMatchMedia } from "@odoo/hoot-mock";
import {
defineActions,
defineModels,
fields,
getService,
models,
mountWithCleanup,
} from "@web/../tests/web_test_helpers";
import { UserMenu } from "@web/webclient/user_menu/user_menu";
import { WebClientOdex } from "@odex30_web/webclient/webclient";
class Partner extends models.Model {
name = fields.Char();
_records = [
{ id: 1, name: "First record" },
{ id: 2, name: "Second record" },
];
_views = {
form: `
<form>
<group>
<field name="name"/>
</group>
</form>
`,
kanban: `
<kanban>
<templates>
<t t-name="card">
<field name="name"/>
</t>
</templates>
</kanban>
`,
list: `<list><field name="name"/></list>`,
};
}
defineModels([Partner]);
defineActions([
{
id: 1,
xml_id: "action_1",
name: "Partners Action 1",
res_model: "partner",
views: [[false, "kanban"]],
},
{
id: 3,
xml_id: "action_3",
name: "Partners",
res_model: "partner",
views: [
[false, "list"],
[false, "kanban"],
[false, "form"],
],
},
]);
describe.current.tags("mobile");
test("scroll position is kept", async () => {
// This test relies on the fact that the scrollable element in mobile
// is view's root node.
const firstRecord = Partner._records[0];
delete firstRecord.id;
Partner._records = [...Array(80)].map((_, i) => ({
...firstRecord,
name: `Record ${i + 1}`,
}));
await mountWithCleanup(WebClientOdex);
await animationFrame();
// partners in list/kanban
await getService("action").doAction(3);
expect(".o_kanban_view").toHaveCount(1);
queryFirst(".o_kanban_view").scrollTo(0, 123);
await click(".o_kanban_record:eq(20)");
await animationFrame();
expect(".o_form_view").toHaveCount(1);
expect(".o_kanban_view").toHaveCount(0);
await click(".o_breadcrumb .o_back_button");
await animationFrame();
expect(".o_form_view").toHaveCount(0);
expect(".o_kanban_view").toHaveCount(1);
});
test("Share URL item is not present in the user menu when screen is small", async () => {
mockMatchMedia({ ["display-mode"]: "standalone" });
await mountWithCleanup(UserMenu);
expect(".o_user_menu").toHaveCount(1);
queryFirst(".o_user_menu").classList.remove("d-none");
await click(".o_user_menu button");
await animationFrame();
expect(".o_user_menu .dropdown-item").toHaveCount(0, {
message: "share button is not visible",
});
});

View File

@ -1,17 +0,0 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("odex30_web.test_studio_list_upsell", {
steps: () => [
{
trigger: ".o_list_view",
},
{
trigger: ".o_optional_columns_dropdown > button",
run: "click",
},
{
trigger: " .o-dropdown--menu .dropdown-item-studio",
},
],
});

View File

@ -1,348 +0,0 @@
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { click, queryAll } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import {
contains,
defineModels,
fields,
getDropdownMenu,
getService,
models,
mountView,
mountWithCleanup,
onRpc,
patchWithCleanup,
} from "@web/../tests/web_test_helpers";
import { browser } from "@web/core/browser/browser";
import { user } from "@web/core/user";
import { WebClientOdex } from "@odex30_web/webclient/webclient";
class Foo extends models.Model {
foo = fields.Char();
bar = fields.Boolean();
_records = [
{ id: 1, bar: true, foo: "yop" },
{ id: 2, bar: true, foo: "blip" },
{ id: 3, bar: true, foo: "gnap" },
{ id: 4, bar: false, foo: "blip" },
];
}
defineModels([Foo]);
const getDefaultConfig = () => ({
actionId: 1,
actionType: "ir.actions.act_window",
});
describe.current.tags("desktop");
beforeEach(() => {
onRpc("has_group", () => true);
});
test("add custom field button with other optional columns - studio not installed", async () => {
expect.assertions(8);
onRpc("search_read", ({ model }) => {
if (model === "ir.module.module") {
expect.step("studio_module_id");
return [{ id: 42 }];
}
});
onRpc("button_immediate_install", ({ model, args }) => {
if (model === "ir.module.module") {
expect(args[0]).toEqual([42], {
message: "Should be the id of studio module returned by the search read",
});
expect.step("studio_module_install");
return true;
}
});
await mountView({
type: "list",
resModel: "foo",
arch: /* xml */ `
<list>
<field name="foo"/>
<field name="bar" optional="hide"/>
</list>
`,
config: getDefaultConfig(),
});
patchWithCleanup(browser.location, {
reload: function () {
expect.step("window_reload");
},
});
expect(".o_data_row").toHaveCount(4);
expect(".o_optional_columns_dropdown_toggle").toHaveCount(1);
await click(".o_optional_columns_dropdown_toggle");
await animationFrame();
const dropdown = getDropdownMenu(".o_optional_columns_dropdown");
expect(queryAll(".dropdown-item", { root: dropdown })).toHaveCount(2);
expect(queryAll(".dropdown-item-studio", { root: dropdown })).toHaveCount(1);
await click(".dropdown-item-studio");
await animationFrame();
expect(".modal-studio").toHaveCount(1);
await click(".modal .o_install_studio");
await animationFrame();
expect(browser.localStorage.getItem("openStudioOnReload")).toBe("main");
expect.verifySteps(["studio_module_id", "studio_module_install", "window_reload"]);
});
test("add custom field button without other optional columns - studio not installed", async () => {
expect.assertions(8);
onRpc("search_read", ({ model }) => {
if (model === "ir.module.module") {
expect.step("studio_module_id");
return [{ id: 42 }];
}
});
onRpc("button_immediate_install", ({ model, args }) => {
if (model === "ir.module.module") {
expect(args[0]).toEqual([42], {
message: "Should be the id of studio module returned by the search read",
});
expect.step("studio_module_install");
return true;
}
});
await mountView({
type: "list",
resModel: "foo",
config: getDefaultConfig(),
arch: /* xml */ `
<list>
<field name="foo"/>
<field name="bar"/>
</list>
`,
});
patchWithCleanup(browser.location, {
reload: function () {
expect.step("window_reload");
},
});
expect(".o_data_row").toHaveCount(4);
expect(".o_optional_columns_dropdown_toggle").toHaveCount(1);
await click(".o_optional_columns_dropdown_toggle");
await animationFrame();
const dropdown = getDropdownMenu(".o_optional_columns_dropdown");
expect(queryAll(".dropdown-item", { root: dropdown })).toHaveCount(1);
expect(queryAll(".dropdown-item-studio", { root: dropdown })).toHaveCount(1);
await click(".dropdown-item-studio");
await animationFrame();
expect(".modal-studio").toHaveCount(1);
await click(".modal .o_install_studio");
await animationFrame();
expect(browser.localStorage.getItem("openStudioOnReload")).toBe("main");
expect.verifySteps(["studio_module_id", "studio_module_install", "window_reload"]);
});
test("add custom field button not shown to non-system users (with opt. col.)", async () => {
expect.assertions(3);
patchWithCleanup(user, { isSystem: false });
await mountView({
type: "list",
resModel: "foo",
config: getDefaultConfig(),
arch: /* xml */ `
<list>
<field name="foo"/>
<field name="bar" optional="hide"/>
</list>
`,
});
expect(".o_optional_columns_dropdown_toggle").toHaveCount(1);
await click(".o_optional_columns_dropdown_toggle");
await animationFrame();
const dropdown = getDropdownMenu(".o_optional_columns_dropdown");
expect(queryAll(".dropdown-item", { root: dropdown })).toHaveCount(1);
expect(queryAll(".dropdown-item-studio", { root: dropdown })).toHaveCount(0);
});
test("add custom field button not shown to non-system users (wo opt. col.)", async () => {
patchWithCleanup(user, { isSystem: false });
await mountView({
type: "list",
resModel: "foo",
config: getDefaultConfig(),
arch: /* xml */ `
<list>
<field name="foo"/>
<field name="bar"/>
</list>
`,
});
expect(".o_optional_columns_dropdown_toggle").toHaveCount(0);
});
test("add custom field button not shown with invalid action", async () => {
expect.assertions(1);
patchWithCleanup(user, { isSystem: false });
await mountView({
type: "list",
resModel: "foo",
config: { ...getDefaultConfig(), actionId: null },
arch: /* xml */ `
<list>
<field name="foo"/>
<field name="bar"/>
</list>
`,
});
expect(".o_optional_columns_dropdown_toggle").toHaveCount(0);
});
test("add custom field button not shown with bank statement line model", async () => {
class AccountBankStatementLine extends models.Model {
name = fields.Char();
_views = {
kanban: `
<kanban>
<template>
<t t-name="card">
<field name="display_name"/>
</t>
</template>
</kanban>
`,
list: `<list><field name="display_name" /> <field name="name" optional="1" /></list>`,
};
}
defineModels([AccountBankStatementLine]);
expect.assertions(3);
await mountWithCleanup(WebClientOdex);
await animationFrame();
await getService("action").doAction({
xml_id: "test",
id: 1312,
name: "test",
res_id: 1,
res_model: "account.bank.statement.line",
type: "ir.actions.act_window",
views: [[false, "kanban"], [false, "list"]],
});
expect("button.o_switch_view.o_list[data-tooltip='List']").toHaveCount(1);
await contains("button.o_switch_view.o_list[data-tooltip='List']").click();
expect(".o_list_renderer .o_list_controller button.dropdown-toggle").toHaveCount(1);
await contains(".o_list_renderer .o_list_controller button.dropdown-toggle").click();
expect(".dropdown-item-studio").toHaveCount(0);
});
test("x2many should not be editable", async () => {
class Bar extends models.Model {}
defineModels([Bar]);
Foo._fields.o2m = fields.One2many({ relation: "bar" });
await mountView({
type: "form",
resModel: "foo",
arch: /* xml */ `
<form>
<notebook>
<page>
<field name="o2m">
<list>
<field name="display_name"/>
</list>
</field>
</page>
<page><div class="test_empty_page" /></page>
</notebook>
</form>
`,
});
expect(".o_optional_columns_dropdown_toggle").toHaveCount(0);
await click(".nav-link:eq(1)");
await animationFrame();
await click(".nav-link:eq(0)");
await animationFrame();
expect(".o_field_widget").toHaveCount(1);
expect(".o_optional_columns_dropdown_toggle").toHaveCount(0);
});
test("upsell studio feature is not polluted by another view", async () => {
class Partner extends models.Model {
name = fields.Char();
_views = {
list: `<list><field name="display_name" /> <field name="name" optional="1" /></list>`,
};
}
defineModels([Partner]);
await mountWithCleanup(WebClientOdex);
await animationFrame();
await getService("action").doAction({
xml_id: "editable",
id: 999,
type: "ir.actions.act_window",
views: [[false, "list"]],
res_model: "partner",
});
await click(".o_optional_columns_dropdown_toggle");
await animationFrame();
expect(".dropdown-item").toHaveCount(2);
expect(".dropdown-item-studio").toHaveCount(1);
await getService("action").doAction({
id: 99,
xml_id: "in_dialog",
type: "ir.actions.act_window",
views: [[false, "list"]],
res_model: "partner",
target: "new",
});
await click(".modal .o_optional_columns_dropdown_toggle");
await animationFrame();
let dropdown = getDropdownMenu(".modal .o_optional_columns_dropdown");
expect(queryAll(".dropdown-item", { root: dropdown })).toHaveCount(1);
expect(queryAll(".dropdown-item-studio", { root: dropdown })).toHaveCount(0);
await click(".modal-header .btn-close");
await animationFrame();
expect(".modal").toHaveCount(0);
await click(".o_optional_columns_dropdown_toggle");
await animationFrame();
expect(".o-dropdown--menu").toHaveCount(0);
await click(".o_optional_columns_dropdown_toggle");
await animationFrame();
dropdown = getDropdownMenu(".o_optional_columns_dropdown");
expect(queryAll(".dropdown-item", { root: dropdown })).toHaveCount(2);
expect(queryAll(".dropdown-item-studio", { root: dropdown })).toHaveCount(1);
});

View File

@ -1,164 +0,0 @@
import { describe, expect, test } from "@odoo/hoot";
import { click } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import {
defineActions,
defineModels,
getService,
fields,
models,
mountWithCleanup,
onRpc,
stepAllNetworkCalls,
} from "@web/../tests/web_test_helpers";
import { redirect } from "@web/core/utils/urls";
import { WebClientOdex } from "@odex30_web/webclient/webclient";
class Partner extends models.Model {
name = fields.Char();
_records = [
{ id: 1, name: "First record" },
{ id: 2, name: "Second record" },
];
_views = {
form: `
<form>
<group>
<field name="name"/>
</group>
</form>
`,
kanban: `
<kanban>
<templates>
<t t-name="card">
<field name="name"/>
</t>
</templates>
</kanban>
`,
list: `<list><field name="name"/></list>`,
};
}
defineModels([Partner]);
defineActions([
{
id: 1,
name: "Partners Action 1",
res_model: "partner",
views: [
[false, "list"],
[false, "kanban"],
[false, "form"],
],
},
{
id: 2,
name: "Partners Action 2",
res_model: "partner",
views: [
[false, "list"],
[false, "form"],
],
},
]);
describe.current.tags("mobile");
test("uses a mobile-friendly view by default (if possible)", async () => {
onRpc("has_group", () => true);
await mountWithCleanup(WebClientOdex);
await animationFrame();
// should default on a mobile-friendly view (kanban) for action 1
await getService("action").doAction(1);
expect(".o_list_view").toHaveCount(0);
expect(".o_kanban_view").toHaveCount(1);
// there is no mobile-friendly view for action 2, should use the first one (list)
await getService("action").doAction(2);
expect(".o_list_view").toHaveCount(1);
expect(".o_kanban_view").toHaveCount(0);
});
test("lazy load mobile-friendly view", async () => {
stepAllNetworkCalls();
redirect("/odoo/action-1/new");
await mountWithCleanup(WebClientOdex);
await animationFrame();
expect(".o_list_view").toHaveCount(0);
expect(".o_kanban_view").toHaveCount(0);
expect(".o_form_view").toHaveCount(1);
// go back to lazy loaded view
await click(".o_breadcrumb .o_back_button");
await animationFrame();
expect(".o_list_view").toHaveCount(0);
expect(".o_form_view").toHaveCount(0);
expect(".o_kanban_view").toHaveCount(1);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"onchange", // default_get/onchange to open form view
"web_search_read", // web search read when coming back to Kanban
]);
});
test("lazy load mobile-friendly view; legacy url", async () => {
stepAllNetworkCalls();
redirect("/web#action=1&view_type=form");
await mountWithCleanup(WebClientOdex);
await animationFrame();
expect(".o_list_view").toHaveCount(0);
expect(".o_kanban_view").toHaveCount(0);
expect(".o_form_view").toHaveCount(1);
// go back to lazy loaded view
await click(".o_breadcrumb .o_back_button");
await animationFrame();
expect(".o_list_view").toHaveCount(0);
expect(".o_form_view").toHaveCount(0);
expect(".o_kanban_view").toHaveCount(1);
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"/web/action/load",
"get_views",
"onchange", // default_get/onchange to open form view
"web_search_read", // web search read when coming back to Kanban
]);
});
test("view switcher button should be displayed in dropdown on mobile screens", async () => {
// This test will spawn a kanban view (mobile friendly).
// so, the "legacy" code won't be tested here.
await mountWithCleanup(WebClientOdex);
await animationFrame();
await getService("action").doAction(1);
expect(".o_control_panel .o_cp_switch_buttons > button").toHaveCount(1);
expect(".o_control_panel .o_cp_switch_buttons .o_switch_view.o_kanban").toHaveCount(0);
expect(".o_control_panel .o_cp_switch_buttons button.o_switch_view").toHaveCount(0);
expect(".o_control_panel .o_cp_switch_buttons > button > i").toHaveClass("oi-view-kanban");
await click(".o_control_panel .o_cp_switch_buttons > button");
await animationFrame();
expect(".dropdown-item:has(.oi-view-kanban)").toHaveClass("selected");
expect(".dropdown-item:has(.oi-view-list)").not.toHaveClass("selected");
});

View File

@ -1,176 +0,0 @@
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { Deferred, mockDate } from "@odoo/hoot-mock";
import {
defineActions,
defineMenus,
defineModels,
fields,
models,
mountWithCleanup,
onRpc,
patchWithCleanup,
} from "@web/../tests/web_test_helpers";
import { browser } from "@web/core/browser/browser";
import { WebClient } from "@web/webclient/webclient";
import { SUCCESS_SIGNAL } from "@web/webclient/clickbot/clickbot";
class Foo extends models.Model {
foo = fields.Char();
bar = fields.Boolean();
date = fields.Date();
_records = [
{ id: 1, bar: true, foo: "yop", date: "2017-01-25" },
{ id: 2, bar: true, foo: "blip" },
{ id: 3, bar: true, foo: "gnap" },
{ id: 4, bar: false, foo: "blip" },
];
_views = {
search: /* xml */ `
<search>
<filter string="Not Bar" name="not bar" domain="[['bar','=',False]]"/>
<filter string="Date" name="date" date="date"/>
</search>
`,
list: /* xml */ `
<list>
<field name="foo" />
</list>
`,
kanban: /* xml */ `
<kanban class="o_kanban_test">
<templates><t t-name="card">
<field name="foo"/>
</t></templates>
</kanban>
`,
};
}
describe.current.tags("desktop");
defineModels([Foo]);
beforeEach(() => {
defineActions([
{
id: 1001,
name: "App1",
res_model: "foo",
views: [
[false, "list"],
[false, "kanban"],
],
xml_id: "app1",
},
{
id: 1002,
name: "App2 Menu 1",
res_model: "foo",
views: [[false, "kanban"]],
xml_id: "app2_menu1",
},
{
id: 1022,
name: "App2 Menu 2",
res_model: "foo",
views: [[false, "list"]],
xml_id: "app2_menu2",
},
]);
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",
},
]);
});
test("clickbot clickeverywhere test", async () => {
onRpc("has_group", () => true);
mockDate("2017-10-08T15:35:11.000");
const clickEverywhereDef = new Deferred();
patchWithCleanup(browser, {
console: {
log: (msg) => {
expect.step(msg);
if (msg === SUCCESS_SIGNAL) {
clickEverywhereDef.resolve();
}
},
error: (msg) => {
expect.step(msg);
clickEverywhereDef.resolve();
},
},
});
const webClient = await mountWithCleanup(WebClient);
patchWithCleanup(odoo, {
__WOWL_DEBUG__: { root: webClient },
});
window.clickEverywhere();
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"',
"Clicking on: apps menu toggle button",
"Testing app menu: app2",
"Testing menu App2 app2",
'Clicking on: menu item "App2"',
"Testing 2 filters",
'Clicking on: filter "Not Bar"',
'Clicking on: filter "Date"',
'Clicking on: filter option "October"',
"Testing menu menu 1 app2_menu1",
'Clicking on: menu item "menu 1"',
"Testing 2 filters",
'Clicking on: filter "Not Bar"',
'Clicking on: filter "Date"',
'Clicking on: filter option "October"',
"Testing menu menu 2 app2_menu2",
'Clicking on: menu item "menu 2"',
"Testing 2 filters",
'Clicking on: filter "Not Bar"',
'Clicking on: filter "Date"',
'Clicking on: filter option "October"',
"Successfully tested 2 apps",
"Successfully tested 2 menus",
"Successfully tested 0 modals",
"Successfully tested 10 filters",
SUCCESS_SIGNAL,
]);
});

View File

@ -1,615 +0,0 @@
import { expect, test } from "@odoo/hoot";
import { click, queryFirst, edit } from "@odoo/hoot-dom";
import { animationFrame, mockDate, runAllTimers } from "@odoo/hoot-mock";
import {
getService,
mockService,
mountWithCleanup,
onRpc,
patchWithCleanup,
serverState,
} from "@web/../tests/web_test_helpers";
import { session } from "@web/session";
import { browser } from "@web/core/browser/browser";
import { WebClientOdex } from "@odex30_web/webclient/webclient";
test("Expiration Panel one app installed", async () => {
mockDate("2019-10-10T12:00:00");
patchWithCleanup(session, {
expiration_date: "2019-11-09 12:00:00",
expiration_reason: "",
storeData: true,
warning: "admin",
});
await mountWithCleanup(WebClientOdex);
await animationFrame();
await getService("action").doAction("menu");
expect(".oe_instance_register").toHaveText("This database will expire in 1 month.");
expect(".database_expiration_panel").toHaveClass("alert-info");
// Close the expiration panel
await click(".oe_instance_hide_panel");
await animationFrame();
expect(".database_expiration_panel").toHaveCount(0);
});
test("Expiration Panel one app installed, buy subscription", async () => {
expect.assertions(6);
mockDate("2019-10-10T12:00:00");
patchWithCleanup(session, {
expiration_date: "2019-10-24 12:00:00",
expiration_reason: "demo",
storeData: true, // used by subscription service to know whether mail is installed
warning: "admin",
});
onRpc("res.users", "search_count", () => 7);
await mountWithCleanup(WebClientOdex);
await animationFrame();
await runAllTimers();
expect(".oe_instance_register").toHaveText(
"This demo database will expire in 14 days. Register your subscription or buy a subscription."
);
expect(".database_expiration_panel").toHaveClass("alert-warning", {
message: "Color should be orange",
});
expect(".oe_instance_register_show").toHaveCount(1, {
message: "Part 'Register your subscription'",
});
expect(".oe_instance_buy").toHaveCount(1, { message: "Part 'buy a subscription'" });
expect(".oe_instance_register_form").toHaveCount(0, {
message: "There should be no registration form",
});
// Click on 'buy subscription'
await click(".oe_instance_buy");
await animationFrame();
expect(browser.location.href).toBe("https://www.odoo.com/odoo-enterprise/upgrade?num_users=7");
});
test("Expiration Panel one app installed, try several times to register subscription", async () => {
expect.assertions(33);
mockDate("2019-10-10T12:00:00");
let callToGetParamCount = 0;
patchWithCleanup(session, {
expiration_date: "2019-10-15 12:00:00",
expiration_reason: "trial",
storeData: true, // used by subscription service to know whether mail is installed
warning: "admin",
});
mockService("notification", {
add: (message, options) => {
expect.step(JSON.stringify({ message, options }));
},
});
onRpc("get_param", ({ args }) => {
expect.step("get_param");
if (args[0] === "database.already_linked_subscription_url") {
return false;
}
if (args[0] === "database.already_linked_email") {
return "super_company_admin@gmail.com";
}
expect(args[0]).toBe("database.expiration_date");
callToGetParamCount++;
if (callToGetParamCount <= 3) {
return "2019-10-15 12:00:00";
} else {
return "2019-11-15 12:00:00";
}
});
onRpc("set_param", ({ args }) => {
expect.step("set_param");
expect(args[0]).toBe("database.enterprise_code");
if (callToGetParamCount === 1) {
expect(args[1]).toBe("ABCDEF");
} else {
expect(args[1]).toBe("ABC");
}
return true;
});
onRpc("update_notification", ({ args }) => {
expect.step("update_notification");
expect(args[0]).toBeInstanceOf(Array);
expect(args[0]).toHaveLength(0);
return true;
});
onRpc("res.users", "search_count", () => 7);
await mountWithCleanup(WebClientOdex);
await animationFrame();
expect(".oe_instance_register").toHaveText(
"This database will expire in 5 days. Register your subscription or buy a subscription."
);
expect(".database_expiration_panel").toHaveClass("alert-danger", {
message: "Color should be red",
});
expect(".oe_instance_register_show").toHaveCount(1, {
message: "Part 'Register your subscription'",
});
expect(".oe_instance_buy").toHaveCount(1, { message: "Part 'buy a subscription'" });
expect(".oe_instance_register_form").toHaveCount(0, {
message: "There should be no registration form",
});
// Click on 'buy subscription'
await click(".oe_instance_register_show");
await animationFrame();
expect(".oe_instance_register_form").toHaveCount(1, {
message: "there should be a registration form",
});
expect('.oe_instance_register_form input[placeholder="Paste code here"]').toHaveCount(1, {
message: "with an input with place holder 'Paste code here'",
});
expect(".oe_instance_register_form button").toHaveCount(1, {
message: "and a button 'Register'",
});
expect(".oe_instance_register_form button").toHaveText("Register");
await click(".oe_instance_register_form button");
await animationFrame();
expect(".oe_instance_register_form").toHaveCount(1, {
message: "there should be a registration form",
});
expect('.oe_instance_register_form input[placeholder="Paste code here"]').toHaveCount(1, {
message: "with an input with place holder 'Paste code here'",
});
expect(".oe_instance_register_form button").toHaveCount(1, {
message: "and a button 'Register'",
});
await click(".oe_instance_register_form input");
await edit("ABCDEF");
await animationFrame();
await click(".oe_instance_register_form button");
await animationFrame();
expect(queryFirst(".oe_instance_register")).toHaveText(
"Something went wrong while registering your database. You can try again or contact Odoo Support."
);
expect(".database_expiration_panel").toHaveClass("alert-danger", {
message: "Color should be red",
});
expect("span.oe_instance_error").toHaveCount(1);
expect(".oe_instance_register_form").toHaveCount(1, {
message: "there should be a registration form",
});
expect('.oe_instance_register_form input[placeholder="Paste code here"]').toHaveCount(1, {
message: "with an input with place holder 'Paste code here'",
});
expect(".oe_instance_register_form button").toHaveCount(1, {
message: "and a button 'Register'",
});
expect(".oe_instance_register_form button").toHaveText("Retry");
await click(".oe_instance_register_form input");
await edit("ABC");
await animationFrame();
await click(".oe_instance_register_form button");
await animationFrame();
expect(".database_expiration_panel").toHaveCount(0, {
message: "expiration panel should be gone",
});
expect.verifySteps([
// second try to submit
"get_param",
"set_param",
"update_notification",
"get_param",
"get_param",
"get_param",
// third try
"get_param",
"set_param",
"update_notification",
"get_param",
"get_param",
"get_param",
`{"message":"Thank you, your registration was successful! Your database is valid until November 15, 2019.","options":{"type":"success"}}`,
]);
});
test("Expiration Panel one app installed, subscription already linked", async () => {
expect.assertions(5);
mockDate("2019-10-10T12:00:00");
let getExpirationDateCount = 0;
patchWithCleanup(session, {
expiration_date: "2019-10-15 12:00:00",
expiration_reason: "trial",
storeData: true, // used by subscription service to know whether mail is installed
warning: "admin",
});
onRpc("/already/linked/send/mail/url", () => ({
result: false,
reason: "By design",
}));
onRpc("get_param", ({ args }) => {
expect.step("get_param");
if (args[0] === "database.expiration_date") {
getExpirationDateCount++;
if (getExpirationDateCount === 1) {
return "2019-10-15 12:00:00";
} else {
return "2019-11-17 12:00:00";
}
}
if (args[0] === "database.already_linked_subscription_url") {
return "www.super_company.com";
}
if (args[0] === "database.already_linked_send_mail_url") {
return "/already/linked/send/mail/url";
}
if (args[0] === "database.already_linked_email") {
return "super_company_admin@gmail.com";
}
});
onRpc("set_param", () => {
expect.step("set_param");
return true;
});
onRpc("update_notification", () => {
expect.step("update_notification");
return true;
});
await mountWithCleanup(WebClientOdex);
await animationFrame();
expect(".oe_instance_register").toHaveText(
"This database will expire in 5 days. Register your subscription or buy a subscription."
);
// Click on 'register your subscription'
await click(".oe_instance_register_show");
await animationFrame();
await click(".oe_instance_register_form input");
await edit("ABC");
await click(".oe_instance_register_form button");
await animationFrame();
expect(".oe_instance_register.oe_database_already_linked").toHaveText(
`Your subscription is already linked to a database.\nSend an email to the subscription owner to confirm the change, enter a new code or buy a subscription.`
);
await click("a.oe_contract_send_mail");
await animationFrame();
expect(".database_expiration_panel").toHaveClass("alert-danger", {
message: "Color should be red",
});
expect(".oe_instance_register.oe_database_already_linked").toHaveText(
`Your subscription is already linked to a database.\nSend an email to the subscription owner to confirm the change, enter a new code or buy a subscription.\n\nUnable to send the instructions by email, please contact the Odoo Support\nError reason: By design`
);
expect.verifySteps([
"get_param",
"set_param",
"update_notification",
"get_param",
"get_param",
"get_param",
"get_param",
]);
});
test("One app installed, database expired", async () => {
expect.assertions(8);
mockDate("2019-10-10T12:00:00");
let callToGetParamCount = 0;
patchWithCleanup(session, {
expiration_date: "2019-10-08 12:00:00",
expiration_reason: "trial",
storeData: true, // used by subscription service to know whether mail is installed
warning: "admin",
});
onRpc("/already/linked/send/mail/url", () => ({
result: false,
reason: "By design",
}));
onRpc("get_param", ({ args }) => {
if (args[0] === "database.already_linked_subscription_url") {
return false;
}
callToGetParamCount++;
if (callToGetParamCount === 1) {
return "2019-10-09 12:00:00";
} else {
return "2019-11-09 12:00:00";
}
});
onRpc("set_param", () => true);
onRpc("update_notification", () => true);
await mountWithCleanup(WebClientOdex);
await animationFrame();
expect(".oe_instance_register").toHaveText(
"This database has expired. Register your subscription or buy a subscription."
);
expect(".o_blockUI").toHaveCount(1, { message: "UI should be blocked" });
expect(".database_expiration_panel").toHaveClass("alert-danger", {
message: "Color should be red",
});
expect(".oe_instance_register_show").toHaveCount(1, {
message: "Part 'Register your subscription'",
});
expect(".oe_instance_buy").toHaveCount(1, { message: "Part 'buy a subscription'" });
expect(".oe_instance_register_form").toHaveCount(0);
// Click on 'Register your subscription'
await click(".oe_instance_register_show");
await animationFrame();
await click(".oe_instance_register_form input");
await edit("ABC");
await click(".oe_instance_register_form button");
await animationFrame();
expect(".oe_instance_register").toHaveText(
"Thank you, your registration was successful! Your database is valid until November 9, 2019."
);
expect(".o_blockUI").toHaveCount(0, { message: "UI should no longer be blocked" });
});
test("One app installed, renew", async () => {
expect.assertions(7);
mockDate("2019-10-10T12:00:00");
patchWithCleanup(session, {
expiration_date: "2019-10-20 12:00:00",
expiration_reason: "renewal",
storeData: true, // used by subscription service to know whether mail is installed
warning: "admin",
});
onRpc("get_param", ({ args }) => {
expect.step("get_param");
expect(args[0]).toBe("database.enterprise_code");
return "ABC";
});
await mountWithCleanup(WebClientOdex);
await animationFrame();
expect(".oe_instance_register").toHaveText(
"Your subscription expired 9 days ago. This database will be blocked soon.\n" +
"Renew now\n" +
"I paid, please recheck!"
);
expect(".database_expiration_panel").toHaveClass("alert-warning", {
message: "Color should be orange",
});
expect(".oe_instance_renew").toHaveCount(1, { message: "Part 'Register your subscription'" });
expect("a.check_enterprise_status").toHaveCount(1, {
message: "there should be a button for status checking",
});
expect(".oe_instance_register_form").toHaveCount(0);
// Click on 'Renew your subscription'
await click(".oe_instance_renew");
await animationFrame();
expect.verifySteps(["get_param"]);
});
test("One app installed, check status and get success", async () => {
expect.assertions(4);
mockDate("2019-10-10T12:00:00");
patchWithCleanup(session, {
expiration_date: "2019-10-20 12:00:00",
expiration_reason: "renewal",
storeData: true, // used by subscription service to know whether mail is installed
warning: "admin",
});
onRpc("get_param", ({ args }) => {
expect.step("get_param");
expect(args[0]).toBe("database.expiration_date");
return "2019-10-24 12:00:00";
});
onRpc("update_notification", () => {
expect.step("update_notification");
return true;
});
await mountWithCleanup(WebClientOdex);
await animationFrame();
// click on "I paid, please recheck!"
expect("a.check_enterprise_status").toHaveText("I paid, please recheck!");
await click("a.check_enterprise_status");
await animationFrame();
expect(".oe_instance_register.oe_subscription_updated").toHaveText(
"Your subscription was updated and is valid until October 24, 2019."
);
expect.verifySteps(["update_notification", "get_param"]);
});
// Why would we want to reload the page when we check the status and it hasn't changed?
test.skip("One app installed, check status and get page reload", async () => {
expect.assertions(4);
mockDate("2019-10-10T12:00:00");
patchWithCleanup(session, {
expiration_date: "2019-10-20 12:00:00",
expiration_reason: "renewal",
storeData: true, // used by subscription service to know whether mail is installed
warning: "admin",
});
onRpc("get_param", () => {
expect.step("get_param");
return "2019-10-20 12:00:00";
});
onRpc("update_notification", () => {
expect.step("update_notification");
return true;
});
await mountWithCleanup(WebClientOdex);
await animationFrame();
// click on "I paid, please recheck!"
await click("a.check_enterprise_status");
await animationFrame();
expect.verifySteps(["update_notification", "get_param", "reloadPage"]);
});
test("One app installed, upgrade database", async () => {
expect.assertions(4);
mockDate("2019-10-10T12:00:00");
patchWithCleanup(session, {
expiration_date: "2019-10-20 12:00:00",
expiration_reason: "upsell",
storeData: true, // used by subscription service to know whether mail is installed
warning: "admin",
});
onRpc("get_param", ({ args }) => {
expect.step("get_param");
expect(args[0]).toBe("database.enterprise_code");
return "ABC";
});
onRpc("search_count", () => {
expect.step("search_count");
return 13;
});
onRpc("update_notification", () => true);
await mountWithCleanup(WebClientOdex);
await animationFrame();
await runAllTimers();
expect(".oe_instance_register").toHaveText(
"This database will expire in 10 days. You have more users or more apps installed than your subscription allows.\n\n" +
"Upgrade your subscription\n" +
"I paid, please recheck!"
);
await click("a.oe_instance_upsell");
await animationFrame();
expect.verifySteps(["get_param", "search_count"]);
expect(browser.location.href).toBe(
"https://www.odoo.com/odoo-enterprise/upsell?num_users=13&contract=ABC"
);
});
test("One app installed, message for non admin user", async () => {
expect.assertions(2);
mockDate("2019-10-10T12:00:00");
patchWithCleanup(session, {
expiration_date: "2019-11-08 12:00:00",
expiration_reason: "",
storeData: true, // used by subscription service to know whether mail is installed
warning: "user",
});
await mountWithCleanup(WebClientOdex);
await animationFrame();
expect(".oe_instance_register").toHaveText(
"This database will expire in 29 days. Log in as an administrator to correct the issue."
);
expect(".database_expiration_panel").toHaveClass("alert-info", {
message: "Color should be grey",
});
});
test("One app installed, navigation to renewal page", async () => {
expect.assertions(8);
mockDate("2019-11-10T00:00:00");
patchWithCleanup(session, {
expiration_date: "2019-10-20 12:00:00",
expiration_reason: "renewal",
storeData: true, // used by subscription service to know whether mail is installed
warning: "admin",
});
onRpc("get_param", ({ args }) => {
expect.step("get_param");
expect(args[0]).toBe("database.enterprise_code");
return "ABC";
});
onRpc("update_notification", () => {
expect.step("update_notification");
return true;
});
await mountWithCleanup(WebClientOdex);
await animationFrame();
await runAllTimers();
expect(".oe_instance_register").toHaveText(
"This database has expired.\nRenew now\nI paid, please recheck!"
);
expect(".database_expiration_panel").toHaveClass("alert-danger");
expect(".oe_instance_renew").toHaveCount(1, { message: "Part 'Register your subscription'" });
expect("a.check_enterprise_status").toHaveCount(1, {
message: "there should be a button for status checking",
});
expect(".oe_instance_register_form").toHaveCount(0);
// Click on 'Renew your subscription'
await click(".oe_instance_renew");
await animationFrame();
expect(browser.location.href).toBe("https://www.odoo.com/odoo-enterprise/renew?contract=ABC");
expect.verifySteps(["get_param"]);
});
test("One app installed, different locale (arabic)", async () => {
expect.assertions(1);
mockDate("2019-25-09T12:00:00");
patchWithCleanup(session, {
expiration_date: "2019-10-20 12:00:00",
expiration_reason: "renewal",
storeData: true, // used by subscription service to know whether mail is installed
warning: "admin",
});
serverState.lang = "ar-001";
onRpc("get_param", () => "2019-11-09 12:00:00");
onRpc("update_notification", () => true);
await mountWithCleanup(WebClientOdex);
await animationFrame();
await click("a.check_enterprise_status");
await animationFrame();
expect(".oe_instance_register").toHaveText(
"Your subscription was updated and is valid until ٩ نوفمبر ٢٠١٩."
);
});

View File

@ -1,353 +0,0 @@
import { describe, expect, test } from "@odoo/hoot";
import { click, drag, keyDown, pointerDown, queryFirst } from "@odoo/hoot-dom";
import { advanceTime, animationFrame, mockDate, mockTouch } from "@odoo/hoot-mock";
import {
getService,
mountWithCleanup,
onRpc,
patchWithCleanup,
} from "@web/../tests/web_test_helpers";
import { reactive } from "@odoo/owl";
import { session } from "@web/session";
import { HomeMenu } from "@odex30_web/webclient/home_menu/home_menu";
import { reorderApps } from "@web/webclient/menus/menu_helpers";
async function walkOn(path) {
for (const step of path) {
await keyDown(`${step.shiftKey ? "shift+" : ""}${step.key}`);
await animationFrame();
expect(`.o_menuitem:eq(${step.index})`).toHaveClass("o_focused", {
message: `step ${step.number}`,
});
}
}
const getDefaultHomeMenuProps = () => {
const apps = [
{
actionID: 121,
href: "/odoo/action-121",
appID: 1,
id: 1,
label: "Discuss",
parents: "",
webIcon: false,
xmlid: "app.1",
},
{
actionID: 122,
href: "/odoo/action-122",
appID: 2,
id: 2,
label: "Calendar",
parents: "",
webIcon: false,
xmlid: "app.2",
},
{
actionID: 123,
href: "/odoo/contacts",
appID: 3,
id: 3,
label: "Contacts",
parents: "",
webIcon: false,
xmlid: "app.3",
},
];
return { apps, reorderApps: (order) => reorderApps(apps, order) };
};
describe.current.tags("desktop");
test("ESC Support", async () => {
await mountWithCleanup(HomeMenu, {
props: getDefaultHomeMenuProps(),
});
patchWithCleanup(getService("home_menu"), {
async toggle(show) {
expect.step(`toggle ${show}`);
},
});
await keyDown("escape");
expect.verifySteps(["toggle false"]);
});
test("Click on an app", async () => {
await mountWithCleanup(HomeMenu, {
props: getDefaultHomeMenuProps(),
});
patchWithCleanup(getService("menu"), {
async selectMenu(menu) {
expect.step(`selectMenu ${menu.id}`);
},
});
await click(".o_menuitem:eq(0)");
await animationFrame();
expect.verifySteps(["selectMenu 1"]);
});
test("Display Expiration Panel (no module installed)", async () => {
mockDate("2019-10-09T00:00:00");
patchWithCleanup(session, {
expiration_date: "2019-11-01 12:00:00",
expiration_reason: "",
isMailInstalled: false,
warning: "admin",
});
await mountWithCleanup(HomeMenu, {
props: getDefaultHomeMenuProps(),
});
expect(".database_expiration_panel").toHaveCount(1);
expect(".database_expiration_panel .oe_instance_register").toHaveText(
"You will be able to register your database once you have installed your first app.",
{ message: "There should be an expiration panel displayed" }
);
// Close the expiration panel
await click(".database_expiration_panel .oe_instance_hide_panel");
await animationFrame();
expect(".database_expiration_panel").toHaveCount(0);
});
test("Navigation (only apps, only one line)", async () => {
expect.assertions(8);
const homeMenuProps = {
apps: new Array(3).fill().map((x, i) => {
return {
actionID: 120 + i,
href: "/odoo/act" + (120 + i),
appID: i + 1,
id: i + 1,
label: `0${i}`,
parents: "",
webIcon: false,
xmlid: `app.${i}`,
};
}),
reorderApps: (order) => reorderApps(homeMenuProps.apps, order),
};
await mountWithCleanup(HomeMenu, {
props: homeMenuProps,
});
const path = [
{ number: 0, key: "ArrowDown", index: 0 },
{ number: 1, key: "ArrowRight", index: 1 },
{ number: 2, key: "Tab", index: 2 },
{ number: 3, key: "ArrowRight", index: 0 },
{ number: 4, key: "Tab", shiftKey: true, index: 2 },
{ number: 5, key: "ArrowLeft", index: 1 },
{ number: 6, key: "ArrowDown", index: 1 },
{ number: 7, key: "ArrowUp", index: 1 },
];
await walkOn(path);
});
test("Navigation (only apps, two lines, one incomplete)", async () => {
expect.assertions(19);
const homeMenuProps = {
apps: new Array(8).fill().map((x, i) => {
return {
actionID: 121,
href: "/odoo/action-121",
appID: i + 1,
id: i + 1,
label: `0${i}`,
parents: "",
webIcon: false,
xmlid: `app.${i}`,
};
}),
reorderApps: (order) => reorderApps(homeMenuProps.apps, order),
};
await mountWithCleanup(HomeMenu, {
props: homeMenuProps,
});
const path = [
{ number: 1, key: "ArrowRight", index: 0 },
{ number: 2, key: "ArrowUp", index: 6 },
{ number: 3, key: "ArrowUp", index: 0 },
{ number: 4, key: "ArrowDown", index: 6 },
{ number: 5, key: "ArrowDown", index: 0 },
{ number: 6, key: "ArrowRight", index: 1 },
{ number: 7, key: "ArrowRight", index: 2 },
{ number: 8, key: "ArrowUp", index: 7 },
{ number: 9, key: "ArrowUp", index: 1 },
{ number: 10, key: "ArrowRight", index: 2 },
{ number: 11, key: "ArrowDown", index: 7 },
{ number: 12, key: "ArrowDown", index: 1 },
{ number: 13, key: "ArrowUp", index: 7 },
{ number: 14, key: "ArrowRight", index: 6 },
{ number: 15, key: "ArrowLeft", index: 7 },
{ number: 16, key: "ArrowUp", index: 1 },
{ number: 17, key: "ArrowLeft", index: 0 },
{ number: 18, key: "ArrowLeft", index: 5 },
{ number: 19, key: "ArrowRight", index: 0 },
];
await walkOn(path);
});
test("Navigation and open an app in the home menu", async () => {
expect.assertions(6);
await mountWithCleanup(HomeMenu, {
props: getDefaultHomeMenuProps(),
});
patchWithCleanup(getService("menu"), {
async selectMenu(menu) {
expect.step(`selectMenu ${menu.id}`);
},
});
// No app selected so nothing to open
await keyDown("enter");
expect.verifySteps([]);
const path = [
{ number: 0, key: "ArrowDown", index: 0 },
{ number: 1, key: "ArrowRight", index: 1 },
{ number: 2, key: "Tab", index: 2 },
{ number: 3, key: "shift+Tab", index: 1 },
];
await walkOn(path);
// open first app (Calendar)
await keyDown("enter");
expect.verifySteps(["selectMenu 2"]);
});
test("Reorder apps in home menu using drag and drop", async () => {
const homeMenuProps = {
apps: reactive(
new Array(8).fill().map((x, i) => {
return {
actionID: 121,
href: "/odoo/action-121",
appID: i + 1,
id: i + 1,
label: `0${i}`,
parents: "",
webIcon: false,
xmlid: `app.${i}`,
};
})
),
reorderApps: (order) => reorderApps(homeMenuProps.apps, order),
};
onRpc("set_res_users_settings", () => {
expect.step(`set_res_users_settings`);
return {
id: 1,
homemenu_config: '["app.1","app.2","app.3","app.0","app.4","app.5","app.6","app.7"]',
};
});
await mountWithCleanup(HomeMenu, {
props: homeMenuProps,
});
const { moveTo, drop } = await drag(".o_draggable:first-child");
await advanceTime(250);
expect(".o_draggable:first-child a").not.toHaveClass("o_dragged_app");
await advanceTime(250);
expect(".o_draggable:first-child a").toHaveClass("o_dragged_app");
await moveTo(".o_draggable:first-child", {
position: {
x: 70,
y: 35,
},
relative: true,
});
await drop(".o_draggable:not(.o_dragged):eq(3)");
await animationFrame();
expect.verifySteps(["set_res_users_settings"]);
expect(".o_app:eq(0)").toHaveAttribute("data-menu-xmlid", "app.1", {
message: "first displayed app has app.1 xmlid",
});
expect(".o_app:eq(3)").toHaveAttribute("data-menu-xmlid", "app.0", {
message: "app 0 is now at 4th position",
});
});
test("The HomeMenu input takes the focus when you press a key only if no other element is the activeElement", async () => {
await mountWithCleanup(HomeMenu, {
props: getDefaultHomeMenuProps(),
});
expect(".o_search_hidden").toBeFocused();
const activeElement = document.createElement("div");
getService("ui").activateElement(activeElement);
// remove the focus from the input
const otherInput = document.createElement("input");
queryFirst(".o_home_menu").appendChild(otherInput);
await pointerDown(otherInput);
await pointerDown(document.body);
expect(".o_search_hidden").not.toBeFocused();
await keyDown("a");
await animationFrame();
expect(".o_search_hidden").not.toBeFocused();
getService("ui").deactivateElement(activeElement);
await keyDown("a");
await animationFrame();
expect(".o_search_hidden").toBeFocused();
});
test("The HomeMenu input does not take the focus if it is already on another input", async () => {
await mountWithCleanup(HomeMenu, {
props: getDefaultHomeMenuProps(),
});
expect(".o_search_hidden").toBeFocused();
const otherInput = document.createElement("input");
queryFirst(".o_home_menu").appendChild(otherInput);
await pointerDown(otherInput);
await keyDown("a");
await animationFrame();
expect(".o_search_hidden").not.toBeFocused();
otherInput.remove();
await keyDown("a");
await animationFrame();
expect(".o_search_hidden").toBeFocused();
});
test("The HomeMenu input does not take the focus if it is already on a textarea", async () => {
await mountWithCleanup(HomeMenu, {
props: getDefaultHomeMenuProps(),
});
expect(".o_search_hidden").toBeFocused();
const textarea = document.createElement("textarea");
queryFirst(".o_home_menu").appendChild(textarea);
await pointerDown(textarea);
await keyDown("a");
await animationFrame();
expect(".o_search_hidden").not.toBeFocused();
textarea.remove();
await keyDown("a");
await animationFrame();
expect(".o_search_hidden").toBeFocused();
});
test("home search input shouldn't be focused on touch devices", async () => {
mockTouch(true);
await mountWithCleanup(HomeMenu, {
props: getDefaultHomeMenuProps(),
});
expect(".o_search_hidden").not.toBeFocused({
message: "home menu search input shouldn't have the focus",
});
});

View File

@ -1,798 +0,0 @@
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { click, keyDown, queryAll, queryFirst } from "@odoo/hoot-dom";
import { animationFrame, Deferred, mockMatchMedia } from "@odoo/hoot-mock";
import { Component, onMounted, xml } from "@odoo/owl";
import {
clearRegistry,
contains,
defineActions,
defineMenus,
defineModels,
fields,
getMockEnv,
getService,
models,
mountWithCleanup,
onRpc,
patchWithCleanup,
serverState,
stepAllNetworkCalls,
} from "@web/../tests/web_test_helpers";
import { browser } from "@web/core/browser/browser";
import { router } from "@web/core/browser/router";
import { registry } from "@web/core/registry";
import { config as transitionConfig } from "@web/core/transition";
import { user } from "@web/core/user";
import { redirect } from "@web/core/utils/urls";
import { UserMenu } from "@web/webclient/user_menu/user_menu";
import { shareUrlMenuItem } from "@odex30_web/webclient/share_url/share_url";
import { WebClientOdex } from "@odex30_web/webclient/webclient";
const actionRegistry = registry.category("actions");
/**
* @param {{ env: import("@web/env").OdooEnv }} [options]
*/
async function mountWebClientOdex(options) {
await mountWithCleanup(WebClientOdex, options);
// Wait for visual changes caused by a potential loadState
await animationFrame();
// wait for BlankComponent
await animationFrame();
// wait for the regular rendering
await animationFrame();
}
async function goToHomeMenu() {
await click(".o_menu_toggle");
await animationFrame();
if (getMockEnv().isSmall) {
await click(queryFirst(".o_sidebar_topbar a.btn-primary", { root: document.body }));
await animationFrame();
}
}
defineActions([
{
id: 1,
xml_id: "action_1",
name: "Partners Action 1",
res_model: "partner",
views: [[false, "kanban"]],
},
{
id: 2,
xml_id: "action_2",
type: "ir.actions.server",
},
{
id: 3,
xml_id: "action_3",
name: "Partners",
res_model: "partner",
views: [
[false, "list"],
[false, "kanban"],
[false, "form"],
],
},
{
id: 4,
xml_id: "action_4",
name: "Partners Action 4",
res_model: "partner",
views: [
[false, "kanban"],
[false, "list"],
[false, "form"],
],
},
{
id: 5,
xml_id: "action_5",
name: "Create a Partner",
res_model: "partner",
target: "new",
views: [[false, "form"]],
},
{
id: 6,
xml_id: "action_6",
name: "Partner",
res_id: 2,
res_model: "partner",
target: "inline",
views: [[false, "form"]],
},
{
id: 1001,
tag: "__test__client__action__",
target: "main",
type: "ir.actions.client",
params: { description: "Id 1" },
},
{
id: 1002,
tag: "__test__client__action__",
target: "main",
type: "ir.actions.client",
params: { description: "Id 2" },
},
]);
defineMenus([
{ id: 0 }, // prevents auto-loading the first action
{ id: 1, name: "App1", appID: 1, actionID: 1001, xmlid: "menu_1" },
{ id: 2, name: "App2", appID: 2, actionID: 1002, xmlid: "menu_2" },
]);
class Partner extends models.Model {
name = fields.Char();
foo = fields.Char();
parent_id = fields.Many2one({ relation: "partner" });
child_ids = fields.One2many({ relation: "partner", relation_field: "parent_id" });
_records = [
{ id: 1, name: "First record", foo: "yop", parent_id: 3 },
{ id: 2, name: "Second record", foo: "blip", parent_id: 3 },
{ id: 3, name: "Third record", foo: "gnap", parent_id: 1 },
{ id: 4, name: "Fourth record", foo: "plop", parent_id: 1 },
{ id: 5, name: "Fifth record", foo: "zoup", parent_id: 1 },
];
_views = {
kanban: `
<kanban>
<templates>
<t t-name="card">
<field name="foo"/>
</t>
</templates>
</kanban>
`,
list: `<list><field name="foo"/></list>`,
form: `
<form>
<header>
<button name="object" string="Call method" type="object"/>
<button name="4" string="Execute action" type="action"/>
</header>
<group>
<field name="name"/>
<field name="foo"/>
</group>
</form>
`,
search: `<search><field name="foo" string="Foo"/></search>`,
};
}
defineModels([Partner]);
class TestClientAction extends Component {
static template = xml`
<div class="test_client_action">
ClientAction_<t t-esc="props.action.params?.description"/>
</div>
`;
static props = ["*"];
setup() {
onMounted(() => {
this.env.config.setDisplayName(`Client action ${this.props.action.id}`);
});
}
}
onRpc("has_group", () => true);
beforeEach(() => {
actionRegistry.add("__test__client__action__", TestClientAction);
patchWithCleanup(transitionConfig, { disabled: true });
});
describe("basic flow with home menu", () => {
stepAllNetworkCalls();
onRpc("partner", "get_formview_action", () => ({
type: "ir.actions.act_window",
res_model: "partner",
view_type: "form",
view_mode: "form",
views: [[false, "form"]],
target: "current",
res_id: 2,
}));
defineMenus(
[
{
id: 1,
name: "App1",
appID: 1,
actionID: 4,
xmlid: "menu_1",
},
],
{ mode: "replace" }
);
test("1 -- start up", async () => {
await mountWebClientOdex();
expect.verifySteps(["/web/webclient/translations", "/web/webclient/load_menus"]);
expect(document.body).toHaveClass("o_home_menu_background");
expect(".o_home_menu").toHaveCount(1);
expect(".o_menu_toggle").not.toBeVisible();
expect(".o_app.o_menuitem").toHaveCount(1);
});
test("2 -- navbar updates on displaying an action", async () => {
await mountWebClientOdex();
expect.verifySteps(["/web/webclient/translations", "/web/webclient/load_menus"]);
await contains(".o_app.o_menuitem").click();
await animationFrame();
expect.verifySteps(["/web/action/load", "get_views", "web_search_read"]);
expect(document.body).not.toHaveClass("o_home_menu_background");
expect(".o_home_menu").toHaveCount(0);
expect(".o_kanban_view").toHaveCount(1);
expect(".o_menu_toggle").toBeVisible();
expect(".o_menu_toggle").not.toHaveClass("o_menu_toggle_back");
});
test("3 -- push another action in the breadcrumb", async () => {
await mountWebClientOdex();
expect.verifySteps(["/web/webclient/translations", "/web/webclient/load_menus"]);
await contains(".o_app.o_menuitem").click();
await animationFrame();
expect.verifySteps(["/web/action/load", "get_views", "web_search_read"]);
expect(".o_kanban_view").toHaveCount(1);
await contains(".o_kanban_record").click();
await animationFrame(); // there is another tick to update navbar and destroy HomeMenu
expect.verifySteps(["web_read"]);
expect(".o_menu_toggle").toBeVisible();
expect(".o_form_view").toHaveCount(1);
expect(".o_breadcrumb .active").toHaveText("First record");
});
test.tags("desktop");
test("4 -- push a third action in the breadcrumb", async () => {
Partner._views["form"] = `
<form>
<field name="display_name"/>
<field name="parent_id" open_target="current"/>
</form>
`;
await mountWebClientOdex();
expect.verifySteps(["/web/webclient/translations", "/web/webclient/load_menus"]);
await contains(".o_app.o_menuitem").click();
await animationFrame();
expect.verifySteps(["/web/action/load", "get_views", "web_search_read"]);
expect(".o_kanban_view").toHaveCount(1);
await contains(".o_kanban_record").click();
expect.verifySteps(["web_read"]);
await contains('.o_field_widget[name="parent_id"] .o_external_button', {
visible: false,
}).click();
expect.verifySteps(["get_formview_action", "get_views", "web_read"]);
expect(".o_form_view").toHaveCount(1);
expect(".o_breadcrumb .active").toHaveText("Second record");
// The third one is the active one
expect(".breadcrumb-item").toHaveCount(2);
});
test("5 -- switch to HomeMenu from an action with 2 breadcrumbs", async () => {
Partner._views["form"] = `
<form>
<field name="display_name"/>
<field name="parent_id" open_target="current"/>
</form>
`;
await mountWebClientOdex();
expect.verifySteps(["/web/webclient/translations", "/web/webclient/load_menus"]);
await contains(".o_app.o_menuitem").click();
await animationFrame();
expect.verifySteps(["/web/action/load", "get_views", "web_search_read"]);
expect(".o_kanban_view").toHaveCount(1);
await contains(".o_kanban_record").click();
expect.verifySteps(["web_read"]);
await contains('.o_field_widget[name="parent_id"] .o_external_button', {
visible: false,
}).click();
expect.verifySteps(["get_formview_action", "get_views", "web_read"]);
await goToHomeMenu();
expect.verifySteps([]);
expect(".o_menu_toggle").toHaveClass("o_menu_toggle_back");
expect(".o_home_menu").toHaveCount(1);
expect(".o_form_view").not.toHaveCount();
});
test.tags("desktop");
test("6 -- back to underlying action with many breadcrumbs", async () => {
Partner._views["form"] = `
<form>
<field name="display_name"/>
<field name="parent_id" open_target="current"/>
</form>
`;
await mountWebClientOdex();
expect.verifySteps(["/web/webclient/translations", "/web/webclient/load_menus"]);
await contains(".o_app.o_menuitem").click();
await animationFrame();
expect.verifySteps(["/web/action/load", "get_views", "web_search_read"]);
expect(".o_kanban_view").toHaveCount(1);
await contains(".o_kanban_record").click();
expect.verifySteps(["web_read"]);
await contains('.o_field_widget[name="parent_id"] .o_external_button', {
visible: false,
}).click();
expect.verifySteps(["get_formview_action", "get_views", "web_read"]);
await contains(".o_menu_toggle").click();
// can't click again too soon because of the mutex in home_menu
// service (waiting for the url to be updated)
await animationFrame();
await contains(".o_menu_toggle").click();
expect.verifySteps(["web_read"]);
expect(".o_home_menu").toHaveCount(0);
expect(".o_form_view").toHaveCount(1);
expect(".o_menu_toggle").not.toHaveClass("o_menu_toggle_back");
expect(".o_breadcrumb .active").toHaveText("Second record");
// Third breadcrumb is the active one
expect(".breadcrumb-item").toHaveCount(2);
});
});
test("restore the newly created record in form view", async () => {
defineActions(
[
{
id: 6,
xml_id: "action_6",
name: "Partner",
res_model: "partner",
views: [[false, "form"]],
},
],
{ mode: "replace" }
);
await mountWebClientOdex();
await getService("action").doAction(6);
expect(".o_form_view").toHaveCount(1);
expect(".o_form_view .o_form_editable").toHaveCount(1);
await contains(".o_field_widget[name=name] input").edit("red right hand");
await contains(".o_form_button_save").click();
expect(".o_breadcrumb .active").toHaveText("red right hand");
await goToHomeMenu();
expect(".o_form_view").not.toHaveCount();
// can't click again too soon because of the mutex in home_menu
// service (waiting for the url to be updated)
await animationFrame();
await contains(".o_menu_toggle").click();
expect(".o_form_view").toHaveCount(1);
expect(".o_form_view .o_form_saved").toHaveCount(1);
expect(".o_breadcrumb .active").toHaveText("red right hand");
});
test.tags("desktop");
test("fast clicking on restore (implementation detail)", async () => {
expect.assertions(8);
let doVeryFastClick = false;
class DelayedClientAction extends Component {
static template = xml`<div class='delayed_client_action'>
<button t-on-click="resolve">RESOLVE</button>
</div>`;
static props = ["*"];
setup() {
onMounted(() => {
if (doVeryFastClick) {
doVeryFastClick = false;
click(".o_menu_toggle"); // go to home menu
}
});
}
}
registry.category("actions").add("DelayedClientAction", DelayedClientAction);
await mountWebClientOdex();
await getService("action").doAction("DelayedClientAction");
await animationFrame();
await contains(".o_menu_toggle").click(); // go to home menu
expect(".o_home_menu").toBeVisible();
expect(".delayed_client_action").not.toHaveCount();
doVeryFastClick = true;
await contains(".o_menu_toggle").click(); // back
expect(".o_home_menu").toHaveCount(0);
expect(".delayed_client_action").toHaveCount(1);
await animationFrame(); // waiting for DelayedClientAction
expect(".o_home_menu").toBeVisible();
expect(".delayed_client_action").not.toHaveCount();
await contains(".o_menu_toggle").click(); // back
await animationFrame();
expect(".o_home_menu").toHaveCount(0);
expect(".delayed_client_action").toHaveCount(1);
});
test("clear unCommittedChanges when toggling home menu", async () => {
expect.assertions(6);
// Edit a form view, don't save, toggle home menu
// the autosave feature of the Form view is activated
// and relied upon by this test
onRpc("web_save", ({ args, model }) => {
expect(model).toBe("partner");
expect(args[1]).toEqual({
name: "red right hand",
foo: false,
});
});
await mountWebClientOdex();
await getService("action").doAction(3, { viewType: "form" });
expect(".o_form_view .o_form_editable").toHaveCount(1);
await contains(".o_field_widget[name=name] input").edit("red right hand");
await goToHomeMenu();
expect(".o_form_view").toHaveCount(0);
expect(".modal").toHaveCount(0);
expect(".o_home_menu").toHaveCount(1);
});
test("can have HomeMenu and dialog action", async () => {
await mountWebClientOdex();
expect(".o_home_menu").toHaveCount(1);
expect(".modal .o_form_view").toHaveCount(0);
await getService("action").doAction(5);
expect(".modal .o_form_view").toHaveCount(1);
expect(".modal .o_form_view").toBeVisible();
expect(".o_home_menu").toHaveCount(1);
});
test("supports attachments of apps deleted", async () => {
// When doing a pg_restore without the filestore
// LPE fixme: may not be necessary anymore since menus are not HomeMenu props anymore
defineMenus([
{
id: 1,
appID: 1,
actionID: 1,
xmlid: "",
name: "Partners",
webIconData: "",
webIcon: "bloop,bloop",
},
]);
serverState.debug = "1";
await mountWebClientOdex();
expect(".o_home_menu").toHaveCount(1);
});
test.tags("desktop");
test("debug manager resets to global items when home menu is displayed", async () => {
const debugRegistry = registry.category("debug");
debugRegistry.category("default").add("item_1", () => ({
type: "item",
description: "globalItem",
callback: () => {},
sequence: 10,
}));
onRpc("has_access", () => true);
serverState.debug = "1";
await mountWebClientOdex();
await contains(".o_debug_manager .dropdown-toggle").click();
expect(".dropdown-item:contains('globalItem')").toHaveCount(1);
expect(".dropdown-item:contains('View: Kanban')").toHaveCount(0);
await contains(".o_debug_manager .dropdown-toggle").click();
await getService("action").doAction(1);
await contains(".o_debug_manager .dropdown-toggle").click();
expect(".dropdown-item:contains('globalItem')").toHaveCount(1);
expect(".dropdown-item:contains('View: Kanban')").toHaveCount(1);
await contains(".o_menu_toggle").click();
await contains(".o_debug_manager .dropdown-toggle").click();
expect(".dropdown-item:contains('globalItem')").toHaveCount(1);
expect(".dropdown-item:contains('View: Kanban')").toHaveCount(0);
await contains(".o_debug_manager .dropdown-toggle").click();
await getService("action").doAction(3);
await contains(".o_debug_manager .dropdown-toggle").click();
expect(".dropdown-item:contains('globalItem')").toHaveCount(1);
expect(".dropdown-item:contains('View: List')").toHaveCount(1);
expect(".dropdown-item:contains('View: Kanban')").toHaveCount(0);
});
test("url state is well handled when going in and out of the HomeMenu", async () => {
patchWithCleanup(browser.location, {
origin: "http://example.com",
});
redirect("/odoo");
await mountWebClientOdex();
expect(router.current).toEqual({
action: "menu",
actionStack: [
{
action: "menu",
displayName: "Home",
},
],
});
expect(browser.history.length).toBe(1);
await contains(".o_apps > .o_draggable:eq(1) > .o_app").click();
await animationFrame();
expect(router.current).toEqual({
action: 1002,
actionStack: [
{
action: 1002,
displayName: "Client action 1002",
},
],
});
expect(browser.history.length).toBe(2);
expect(browser.location.href).toBe("http://example.com/odoo/action-1002");
await goToHomeMenu();
await animationFrame();
expect(router.current).toEqual(
{
action: "menu",
actionStack: [
{
action: 1002,
displayName: "Client action 1002",
},
{
action: "menu",
displayName: "Home",
},
],
},
{
message:
"the actionStack is required to be able to restore the menu toggle back button and the underlying breadcrumbs",
}
);
expect(browser.history.length).toBe(3);
expect(browser.location.href).toBe("http://example.com/odoo", {
message:
"despite the actionStack being in the router state, the url shouldn't have any path",
});
await contains(".o_apps > .o_draggable:eq(0) > .o_app").click();
await animationFrame();
expect(router.current).toEqual(
{
action: 1001,
actionStack: [
{
action: 1001,
displayName: "Client action 1001",
},
],
},
{ message: "clicking another app creates a new action stack (ie empties the breadcrumb)" }
);
expect(browser.history.length).toBe(4);
expect(browser.location.href).toBe("http://example.com/odoo/action-1001");
browser.history.back();
await animationFrame();
expect(router.current).toEqual(
{
action: "menu",
actionStack: [
{
action: 1002,
displayName: "Client action 1002",
},
{
action: "menu",
displayName: "Home",
},
],
globalState: {},
},
{ message: "actionStack was restored" }
);
expect(browser.history.length).toBe(4, {
message: "the previous history entry still exists (available with forward button)",
});
expect(browser.location.href).toBe("http://example.com/odoo");
await contains(".o_menu_toggle").click();
await animationFrame();
expect(router.current).toEqual({
action: 1002,
actionStack: [
{
action: 1002,
displayName: "Client action 1002",
},
],
});
expect(browser.history.length).toBe(4);
expect(browser.location.href).toBe("http://example.com/odoo/action-1002");
});
test.tags("desktop");
test("underlying action's menu items are invisible when HomeMenu is displayed", async () => {
defineMenus([
{
id: 1,
children: [
{
id: 99,
name: "SubMenu",
appID: 1,
actionID: 1002,
xmlid: "",
webIconData: undefined,
webIcon: false,
},
],
},
]);
await mountWebClientOdex();
expect("nav .o_menu_sections").toHaveCount(0);
expect("nav .o_menu_brand").toHaveCount(0);
await contains(".o_app.o_menuitem:nth-child(1)").click();
await animationFrame();
expect("nav .o_menu_sections").toHaveCount(1);
expect("nav .o_menu_brand").toHaveCount(1);
expect(".o_menu_sections").toBeVisible();
expect(".o_menu_brand").toBeVisible();
await contains(".o_menu_toggle").click();
expect("nav .o_menu_sections").toHaveCount(1);
expect("nav .o_menu_brand").toHaveCount(1);
expect(".o_menu_sections").not.toBeVisible();
expect(".o_menu_brand").not.toBeVisible();
});
test("go back to home menu using browser back button", async () => {
await mountWebClientOdex();
expect(".o_home_menu").toHaveCount(1);
expect(".o_main_navbar .o_menu_toggle").not.toBeVisible();
await contains(".o_apps > .o_draggable:nth-child(2) > .o_app").click();
expect(".test_client_action").toHaveCount(0);
await animationFrame();
expect(".test_client_action").toHaveCount(1);
expect(".o_home_menu").toHaveCount(0);
browser.history.back();
await animationFrame();
await animationFrame();
expect(".test_client_action").toHaveCount(0);
expect(".o_home_menu").toHaveCount(1);
expect(".o_main_navbar .o_menu_toggle").not.toBeVisible();
});
test("initial action crashes", async () => {
expect.errors(1);
redirect("/odoo/action-__test__client__action__?menu_id=1");
const ClientAction = registry.category("actions").get("__test__client__action__");
class Override extends ClientAction {
setup() {
super.setup();
expect.step("clientAction setup");
throw new Error("my error");
}
}
registry.category("actions").add("__test__client__action__", Override, { force: true });
await mountWebClientOdex();
expect.verifySteps(["clientAction setup"]);
expect("nav .o_menu_toggle").toHaveCount(1);
expect("nav .o_menu_toggle").toBeVisible();
expect(".o_action_manager").toHaveInnerHTML("");
expect(router.current).toEqual({
action: "__test__client__action__",
menu_id: 1,
actionStack: [
{
action: "__test__client__action__",
},
],
});
await animationFrame();
expect.verifyErrors(["my error"]);
});
test("Apps are reordered at startup based on session's user settings", async () => {
// Config is written with apps xmlids order (default is menu_1, menu_2)
patchWithCleanup(user, {
get settings() {
return { id: 1, homemenu_config: '["menu_2","menu_1"]' };
},
});
await mountWebClientOdex();
const apps = queryAll(".o_app");
expect(apps[0]).toHaveAttribute("data-menu-xmlid", "menu_2", {
message: "first displayed app has menu_2 xmlid",
});
expect(apps[1]).toHaveAttribute("data-menu-xmlid", "menu_1", {
message: "second displayed app has menu_1 xmlid",
});
expect(apps[0]).toHaveText("App2", { message: "first displayed app is App2" });
expect(apps[1]).toHaveText("App1", { message: "second displayed app is App1" });
});
test.tags("desktop");
test("Share URL item is present in the user menu when running as PWA", async () => {
mockMatchMedia({ ["display-mode"]: "standalone" });
clearRegistry(registry.category("user_menuitems"));
// This service adds a "Dark Mode" item to the user menu items on start
registry.category("services").remove("color_scheme");
registry.category("user_menuitems").add("share_url", shareUrlMenuItem);
await mountWithCleanup(UserMenu);
await contains(".o_user_menu button").click();
expect(".o-dropdown--menu .dropdown-item").toHaveCount(1);
expect(".o-dropdown--menu .dropdown-item").toHaveText("Share");
});
test.tags("desktop");
test("Share URL item is not present in the user menu when not running as PWA", async () => {
mockMatchMedia({ ["display-mode"]: "browser" });
clearRegistry(registry.category("user_menuitems"));
// This service adds a "Dark Mode" item to the user menu items on start
registry.category("services").remove("color_scheme");
registry.category("user_menuitems").add("share_url", shareUrlMenuItem);
await mountWithCleanup(UserMenu);
await contains(".o_user_menu button").click();
expect(".o-dropdown--menu .dropdown-item").not.toHaveCount();
});
test("Navigate to an application from the HomeMenu should generate only one pushState", async () => {
patchWithCleanup(history, {
pushState(state, title, url) {
super.pushState(...arguments);
const parsedUrl = new URL(url);
expect.step(parsedUrl.pathname + parsedUrl.search);
},
});
await mountWebClientOdex();
await contains(".o_apps > .o_draggable:nth-child(2) > .o_app").click();
await animationFrame();
expect(".test_client_action").toHaveCount(1);
expect(".test_client_action").toHaveText("ClientAction_Id 2");
await goToHomeMenu();
expect(".o_home_menu").toHaveCount(1);
await contains(".o_apps > .o_draggable:nth-child(1) > .o_app").click();
await animationFrame();
expect(".test_client_action").toHaveCount(1);
expect(".test_client_action").toHaveText("ClientAction_Id 1");
await goToHomeMenu();
await animationFrame();
expect(".o_home_menu").toHaveCount(1);
expect.verifySteps(["/odoo", "/odoo/action-1002", "/odoo", "/odoo/action-1001", "/odoo"]);
});
test.tags("desktop");
test("Should not crash when opening an app via palette and immediately entering input in the palette search", async () => {
await mountWebClientOdex();
const def = new Deferred();
onRpc("web_search_read", () => def);
await keyDown("a");
await animationFrame();
await keyDown("Enter");
await keyDown("a");
await animationFrame();
def.resolve();
await animationFrame();
expect(".test_client_action").toHaveCount(1);
expect(".test_client_action").toHaveText("ClientAction_Id 1");
});

View File

@ -1,2 +0,0 @@
from . import test_odex

View File

@ -1,68 +0,0 @@
import base64
from odoo.tests.common import HttpCase, tagged
class LoadMenusTests(HttpCase):
def setUp(self):
super().setUp()
self.menu = self.env["ir.ui.menu"].create({
"name": "test_menu",
"parent_id": False,
})
def search(*args, **kwargs):
return self.menu
self.patch(type(self.env["ir.ui.menu"]), "search", search)
self.authenticate("admin", "admin")
def test_web_icon(self):
self.menu.web_icon = False
self.menu.web_icon_data = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+BCQAHBQICJmhD1AAAAABJRU5ErkJggg=="
menu_loaded = self.url_open("/web/webclient/load_menus/1234")
expected = {
str(self.menu.id): {
"actionID": False,
"actionModel": False,
"actionPath": False,
"appID": self.menu.id,
"children": [],
"id": self.menu.id,
"name": "test_menu",
"webIcon": False,
"webIconData": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+BCQAHBQICJmhD1AAAAABJRU5ErkJggg==",
"webIconDataMimetype": "image/png",
"xmlid": ""
},
"root": {
"actionID": False,
"actionModel": False,
"actionPath": False,
"appID": False,
"children": [
self.menu.id
],
"id": "root",
"name": "root",
"webIcon": None,
"webIconData": None,
"webIconDataMimetype": None,
"xmlid": "",
"backgroundImage": None,
}
}
self.assertDictEqual(menu_loaded.json(), expected)
@tagged("-at_install", "post_install")
class TestWeb(HttpCase):
def test_studio_list_upsell(self):
invoice_action = self.env.ref("account.action_move_out_invoice_type", raise_if_not_found=False)
if not invoice_action:
return
self.start_tour("/odoo/action-account.action_move_out_invoice_type", "odex30_web.test_studio_list_upsell", login="admin")

View File

@ -1,10 +0,0 @@
import odoo
odoo.release.version_info = odoo.release.version_info[:5] + ('e',)
if '+e' not in odoo.release.version:
odoo.release.version = '{0}+e{1}{2}'.format(*odoo.release.version.partition('-'))
odoo.service.common.RPC_VERSION_1.update(
server_version=odoo.release.version,
server_version_info=odoo.release.version_info)