19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:39 +01:00
parent 38c6088dcc
commit d9452d2060
243 changed files with 30797 additions and 10815 deletions

View file

@ -21,24 +21,30 @@ models which only purpose is to run tests.""",
'data/test_website_demo.xml',
],
'data': [
'security/test_website_security.xml',
'security/ir.model.access.csv',
'views/templates.xml',
'views/test_model_multi_website_views.xml',
'views/test_model_views.xml',
'data/test_website_data.xml',
'security/test_website_security.xml',
'security/ir.model.access.csv',
],
'installable': True,
'assets': {
'test_website.test_bundle': [
'http://test.external.link/javascript1.js',
'/web/static/src/libs/fontawesome/css/font-awesome.css',
'http://test.external.link/style1.css',
'/web/static/src/module_loader.js',
'http://test.external.link/javascript2.js',
'http://test.external.link/style2.css',
],
'web.assets_frontend': [
'test_website/static/src/js/test_error.js',
'test_website/static/src/interactions/**/*',
],
'web.assets_tests': [
'test_website/static/tests/tours/*',
],
'web.qunit_suite_tests': [
'test_website/static/tests/*.js',
],
},
'author': 'Odoo S.A.',
'license': 'LGPL-3',
}

View file

@ -16,6 +16,12 @@ class WebsiteTest(Home):
def test_view(self, **kwargs):
return request.render('test_website.test_view')
@http.route('/test_view_access_error', type='http', auth='public', website=True, sitemap=False)
def test_view_access_error(self, **kwargs):
public = self.env.ref('base.public_user')
record = self.env.ref('test_website.test_model_exposed_record_not_published')
return request.render('test_website.test_view_access_error', {'record': record.with_user(public)})
@http.route('/ignore_args/converteronly/<string:a>', type='http', auth="public", website=True, sitemap=False)
def test_ignore_args_converter_only(self, a):
return request.make_response(json.dumps(dict(a=a, kw=None)))
@ -42,7 +48,7 @@ class WebsiteTest(Home):
@http.route('/multi_company_website', type='http', auth="public", website=True, sitemap=False)
def test_company_context(self):
return request.make_response(json.dumps(request.context.get('allowed_company_ids')))
return request.make_response(json.dumps(request.env.context.get('allowed_company_ids')))
@http.route('/test_lang_url/<model("res.country"):country>', type='http', auth='public', website=True, sitemap=False)
def test_lang_url(self, **kwargs):
@ -50,7 +56,7 @@ class WebsiteTest(Home):
# Test Session
@http.route('/test_get_dbname', type='json', auth='public', website=True, sitemap=False)
@http.route('/test_get_dbname', type='jsonrpc', auth='public', website=True, sitemap=False)
def test_get_dbname(self, **kwargs):
return request.env.cr.dbname
@ -64,7 +70,7 @@ class WebsiteTest(Home):
def test_user_error_http(self, **kwargs):
raise UserError("This is a user http test")
@http.route('/test_user_error_json', type='json', auth='public', website=True, sitemap=False)
@http.route('/test_user_error_json', type='jsonrpc', auth='public', website=True, sitemap=False)
def test_user_error_json(self, **kwargs):
raise UserError("This is a user rpc test")
@ -72,11 +78,11 @@ class WebsiteTest(Home):
def test_validation_error_http(self, **kwargs):
raise ValidationError("This is a validation http test")
@http.route('/test_validation_error_json', type='json', auth='public', website=True, sitemap=False)
@http.route('/test_validation_error_json', type='jsonrpc', auth='public', website=True, sitemap=False)
def test_validation_error_json(self, **kwargs):
raise ValidationError("This is a validation rpc test")
@http.route('/test_access_error_json', type='json', auth='public', website=True, sitemap=False)
@http.route('/test_access_error_json', type='jsonrpc', auth='public', website=True, sitemap=False)
def test_access_error_json(self, **kwargs):
raise AccessError("This is an access rpc test")
@ -84,7 +90,7 @@ class WebsiteTest(Home):
def test_access_error_http(self, **kwargs):
raise AccessError("This is an access http test")
@http.route('/test_missing_error_json', type='json', auth='public', website=True, sitemap=False)
@http.route('/test_missing_error_json', type='jsonrpc', auth='public', website=True, sitemap=False)
def test_missing_error_json(self, **kwargs):
raise MissingError("This is a missing rpc test")
@ -92,7 +98,7 @@ class WebsiteTest(Home):
def test_missing_error_http(self, **kwargs):
raise MissingError("This is a missing http test")
@http.route('/test_internal_error_json', type='json', auth='public', website=True, sitemap=False)
@http.route('/test_internal_error_json', type='jsonrpc', auth='public', website=True, sitemap=False)
def test_internal_error_json(self, **kwargs):
raise werkzeug.exceptions.InternalServerError()
@ -100,7 +106,7 @@ class WebsiteTest(Home):
def test_internal_error_http(self, **kwargs):
raise werkzeug.exceptions.InternalServerError()
@http.route('/test_access_denied_json', type='json', auth='public', website=True, sitemap=False)
@http.route('/test_access_denied_json', type='jsonrpc', auth='public', website=True, sitemap=False)
def test_denied_error_json(self, **kwargs):
raise AccessDenied("This is an access denied rpc test")
@ -131,7 +137,7 @@ class WebsiteTest(Home):
return 'Basic Controller Content'
# Test Redirects
@http.route(['/test_website/country/<model("res.country"):country>'], type='http', auth="public", website=True, sitemap=False)
@http.route(['/test_website/country/<model("res.country"):country>'], type='http', auth="public", website=True, sitemap=True)
def test_model_converter_country(self, country, **kw):
return request.render('test_website.test_redirect_view', {'country': country})
@ -141,6 +147,16 @@ class WebsiteTest(Home):
@http.route(['/test_website/model_item/<int:record_id>'], type='http', methods=['GET'], auth="public", website=True, sitemap=False)
def test_model_item(self, record_id):
record = request.env['test.model'].browse(record_id)
values = {
'record': record,
'main_object': record,
'tag': record.tag_id,
}
return request.render("test_website.model_item", values)
@http.route(['/test_website/model_item_sudo/<int:record_id>'], type='http', methods=['GET'], auth="public", website=True, sitemap=False)
def test_model_item_sudo(self, record_id):
values = {
'record': request.env['test.model'].sudo().browse(record_id),
}

View file

@ -11,8 +11,23 @@
</record>
<!-- SOME DEFAULT TEST.MODEL RECORDS WITH DIFFERENT WEBSITE_ID -->
<record id="test_tag_generic" model="test.tag">
<field name="name">Test Tag</field>
</record>
<record id="test_tag_2" model="test.tag">
<field name="name">Test Tag #2</field>
</record>
<record id="test_tag_3" model="test.tag">
<field name="name">Test Tag #3</field>
</record>
<record id="test_model_generic" model="test.model">
<field name="name">Test Model</field>
<field name="tag_id" ref="test_website.test_tag_generic"/>
</record>
<record id="test_submodel_generic" model="test.submodel">
<field name="name">Test Submodel</field>
<field name="test_model_id" ref="test_website.test_model_generic"/>
<field name="tag_id" ref="test_website.test_tag_generic"/>
</record>
<record id="test_model_multi_generic" model="test.model.multi.website">
<field name="name">Test Multi Model Generic</field>
@ -62,9 +77,9 @@
<div class="row">
<ul class="list-group http_error col-6">
<li class="list-group-item list-group-item-primary"><h2>http Errors</h2></li>
<li class="list-group-item"><a href="/test_user_error_http">http UserError (400)</a></li>
<li class="list-group-item"><a href="/test_validation_error_http">http ValidationError (400)</a></li>
<li class="list-group-item"><a href="/test_missing_error_http">http MissingError (400)</a></li>
<li class="list-group-item"><a href="/test_user_error_http">http UserError (422)</a></li>
<li class="list-group-item"><a href="/test_validation_error_http">http ValidationError (422)</a></li>
<li class="list-group-item"><a href="/test_missing_error_http">http MissingError (404)</a></li>
<li class="list-group-item"><a href="/test_access_error_http">http AccessError (403)</a></li>
<li class="list-group-item"><a href="/test_access_denied_http">http AccessDenied (403)</a></li>
<li class="list-group-item"><a href="/test_internal_error_http">http InternalServerError (500)</a></li>
@ -109,6 +124,9 @@
<p>placeholder</p>
</xpath>
</template>
<template id="test_view_access_error">
<p><t t-out="record.name"/></p>
</template>
<!-- RECORDS FOR MODULE OPERATION TESTS -->
<template id="update_module_base_view">
@ -117,8 +135,8 @@
<!-- RECORDS FOR REDIRECT TESTS -->
<template id="test_redirect_view">
<t t-esc="country.name"/>
<t t-if="not request.env.user._is_public()" t-esc="'Logged In'"/>
<t t-out="country.name"/>
<t t-if="not request.env.user._is_public()" t-out="'Logged In'"/>
<!-- `href` is send through `url_for` for non editor users -->
<a href="/test_website/country/andorra-1">I am a link</a>
</template>
@ -143,5 +161,13 @@
<record id="test_model_record" model="test.model">
<field name="name">Test Model Record</field>
</record>
<record id="test_model_exposed_record_published" model="test.model">
<field name="name">Test Model Exposed Record (published)</field>
<field name="website_published">True</field>
</record>
<record id="test_model_exposed_record_not_published" model="test.model">
<field name="name">Test Model Exposed Record (not published)</field>
<field name="website_published">False</field>
</record>
</data>
</odoo>

View file

@ -1,3 +1,2 @@
from . import model
from . import res_config_settings
from . import website

View file

@ -2,6 +2,13 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.tools.translate import html_translate
class Website(models.Model):
_inherit = "website"
name_translated = fields.Char(translate=True)
class TestModel(models.Model):
@ -13,14 +20,24 @@ class TestModel(models.Model):
]
_description = 'Website Model Test'
name = fields.Char(required=1)
name = fields.Char(required=True, translate=True)
submodel_ids = fields.One2many('test.submodel', 'test_model_id', "Submodels")
website_description = fields.Html(
string="Description for the website",
translate=html_translate,
sanitize_overridable=True,
sanitize_attributes=False,
sanitize_form=False,
default="""<div class="o_test_website_description"><p>A simple website description content.</p></div>""",
)
tag_id = fields.Many2one('test.tag')
@api.model
def _search_get_detail(self, website, order, options):
return {
'model': 'test.model',
'base_domain': [],
'search_fields': ['name'],
'search_fields': ['name', 'submodel_ids.name', 'submodel_ids.tag_id.name'],
'fetch_fields': ['name'],
'mapping': {
'name': {'name': 'name', 'type': 'text', 'match': True},
@ -35,6 +52,22 @@ class TestModel(models.Model):
return self.env['website'].get_client_action(f'/test_model/{self.id}')
class TestSubmodel(models.Model):
_name = 'test.submodel'
_description = 'Website Submodel Test'
name = fields.Char(required=True)
test_model_id = fields.Many2one('test.model')
tag_id = fields.Many2one('test.tag')
class TestTag(models.Model):
_name = 'test.tag'
_description = 'Website Tag Test'
name = fields.Char(required=True)
class TestModelMultiWebsite(models.Model):
_name = 'test.model.multi.website'
_inherit = [
@ -42,9 +75,21 @@ class TestModelMultiWebsite(models.Model):
]
_description = 'Multi Website Model Test'
name = fields.Char(required=1)
name = fields.Char(required=True)
# `cascade` is needed as there is demo data for this model which are bound
# to website 2 (demo website). But some tests are unlinking the website 2,
# which would fail if the `cascade` is not set. Note that the website 2 is
# never set on any records in all other modules.
website_id = fields.Many2one('website', string='Website', ondelete='cascade')
class TestModelExposed(models.Model):
_name = 'test.model.exposed'
_inherit = [
'website.seo.metadata',
'website.published.mixin',
]
_description = 'Website Model Test Exposed'
_rec_name = "name"
name = fields.Char()

View file

@ -1,11 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
def action_website_test_setting(self):
return self.env['website'].get_client_action('/')

View file

@ -1,12 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo import fields, models
from odoo.exceptions import AccessError
class Website(models.Model):
_inherit = "website"
some_translatable_field = fields.Char(string="A translatable field",
translate=True, default='something')
def _search_get_details(self, search_type, order, options):
result = super()._search_get_details(search_type, order, options)
if search_type in ['test']:

View file

@ -1,4 +1,20 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_test_model,access_test_model,model_test_model,,1,0,0,0
access_test_model_multi_website,access_test_model_multi_website,model_test_model_multi_website,,1,0,0,0
access_test_model_public,access_test_model,model_test_model,base.group_public,1,0,0,0
access_test_model_portal,access_test_model,model_test_model,base.group_portal,1,0,0,0
access_test_model_employee,access_test_model,model_test_model,base.group_user,1,0,0,0
access_test_model_admin,access_test_model,model_test_model,base.group_system,1,1,1,1
access_test_submodel_public,access_test_submodel,model_test_submodel,base.group_public,1,0,0,0
access_test_submodel_portal,access_test_submodel,model_test_submodel,base.group_portal,1,0,0,0
access_test_submodel_employee,access_test_submodel,model_test_submodel,base.group_user,1,0,0,0
access_test_tag_public,access_test_tag,model_test_tag,base.group_public,1,0,0,0
access_test_tag_portal,access_test_tag,model_test_tag,base.group_portal,1,0,0,0
access_test_tag_employee,access_test_tag,model_test_tag,base.group_user,1,0,0,0
access_test_model_test_admin,access_test_model,model_test_model,test_website.group_test_website_admin,1,1,1,1
access_test_model_exposed_employee,access_test_model_exposed,model_test_model_exposed,base.group_user,1,0,0,0
access_test_model_exposed_test_admin,access_test_model_exposed,model_test_model_exposed,test_website.group_test_website_admin,1,1,1,1
access_test_model_multi_website_public,access_test_model_multi_website,model_test_model_multi_website,base.group_public,1,0,0,0
access_test_model_multi_website_portal,access_test_model_multi_website,model_test_model_multi_website,base.group_portal,1,0,0,0
access_test_model_multi_website_employee,access_test_model_multi_website,model_test_model_multi_website,base.group_user,1,0,0,0
access_test_model_multi_website_test_admin,access_test_model_multi_website,model_test_model_multi_website,test_website.group_test_website_admin,1,1,1,1
access_test_model_multi_website,access_test_model_multi_website,model_test_model_multi_website,base.group_user,1,0,0,0
access_test_model_tester,access_test_model,model_test_model,test_website.group_test_website_tester,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_test_model access_test_model_public access_test_model model_test_model base.group_public 1 0 0 0
3 access_test_model_multi_website access_test_model_portal access_test_model_multi_website access_test_model model_test_model_multi_website model_test_model base.group_portal 1 0 0 0
4 access_test_model_employee access_test_model model_test_model base.group_user 1 0 0 0
5 access_test_model_admin access_test_model model_test_model base.group_system 1 1 1 1
6 access_test_submodel_public access_test_submodel model_test_submodel base.group_public 1 0 0 0
7 access_test_submodel_portal access_test_submodel model_test_submodel base.group_portal 1 0 0 0
8 access_test_submodel_employee access_test_submodel model_test_submodel base.group_user 1 0 0 0
9 access_test_tag_public access_test_tag model_test_tag base.group_public 1 0 0 0
10 access_test_tag_portal access_test_tag model_test_tag base.group_portal 1 0 0 0
11 access_test_tag_employee access_test_tag model_test_tag base.group_user 1 0 0 0
12 access_test_model_test_admin access_test_model model_test_model test_website.group_test_website_admin 1 1 1 1
13 access_test_model_exposed_employee access_test_model_exposed model_test_model_exposed base.group_user 1 0 0 0
14 access_test_model_exposed_test_admin access_test_model_exposed model_test_model_exposed test_website.group_test_website_admin 1 1 1 1
15 access_test_model_multi_website_public access_test_model_multi_website model_test_model_multi_website base.group_public 1 0 0 0
16 access_test_model_multi_website_portal access_test_model_multi_website model_test_model_multi_website base.group_portal 1 0 0 0
17 access_test_model_multi_website_employee access_test_model_multi_website model_test_model_multi_website base.group_user 1 0 0 0
18 access_test_model_multi_website_test_admin access_test_model_multi_website model_test_model_multi_website test_website.group_test_website_admin 1 1 1 1
19 access_test_model_multi_website access_test_model_multi_website model_test_model_multi_website base.group_user 1 0 0 0
20 access_test_model_tester access_test_model model_test_model test_website.group_test_website_tester 1 1 1 1

View file

@ -1,16 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.module.category" id="test_website.module_category_test_website">
<field name="name">Tests about Website with additional model</field>
<field name="sequence">24</field>
<record id="test_website.group_test_website_admin" model="res.groups">
<field name="name">Test Administrator</field>
</record>
<record id="group_test_website_tester" model="res.groups">
<field name="name">Tester</field>
<field name="category_id" ref="test_website.module_category_test_website"/>
<field name="comment">Tests about Website with additional model</field>
</record>
<record id="base.user_admin" model="res.users">
<field name="groups_id" eval="[(4, ref('test_website.group_test_website_tester'))]"/>
<field name="group_ids" eval="[(4,ref('test_website.group_test_website_tester'))]"/>
</record>
</odoo>

View file

@ -0,0 +1,15 @@
import { Interaction } from "@web/public/interaction";
import { registry } from "@web/core/registry";
import { rpc } from "@web/core/network/rpc";
export class TestError extends Interaction {
static selector = ".rpc_error a";
dynamicContent = {
_root: { "t-on-click.prevent": () => rpc(this.el.getAttribute("href")) },
}
}
registry
.category("public.interactions")
.add("test_website.test_error", TestError);

View file

@ -1,30 +0,0 @@
odoo.define('website_forum.test_error', function (require) {
'use strict';
var publicWidget = require('web.public.widget');
publicWidget.registry.testError = publicWidget.Widget.extend({
selector: '.rpc_error',
events: {
'click a': '_onRpcErrorClick',
},
//----------------------------------------------------------------------
// Handlers
//----------------------------------------------------------------------
/**
* make a rpc call with the href of the DOM element clicked
* @private
* @param {Event} ev
* @returns {Promise}
*/
_onRpcErrorClick: function (ev) {
ev.preventDefault();
var $link = $(ev.currentTarget);
return this._rpc({
route: $link.attr('href'),
});
}
});
});

View file

@ -1,137 +0,0 @@
/** @odoo-module **/
import { FileSelectorControlPanel } from '@web_editor/components/media_dialog/file_selector';
import { getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
import { HtmlField } from '@web_editor/js/backend/html_field';
import {registry} from '@web/core/registry';
import testUtils from 'web.test_utils';
import { uploadService } from '@web_editor/components/upload_progress_toast/upload_service';
import { unsplashService } from '@web_unsplash/services/unsplash_service';
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import weTestUtils from 'web_editor.test_utils';
import Wysiwyg from 'web_editor.wysiwyg';
const { useEffect } = owl;
QUnit.module('field html file upload', {
beforeEach: function () {
this.data = weTestUtils.wysiwygData({
'mail.compose.message': {
fields: {
display_name: {
string: "Displayed name",
type: "char"
},
body: {
string: "Message Body inline (to send)",
type: "html"
},
attachment_ids: {
string: "Attachments",
type: "many2many",
relation: "ir.attachment",
}
},
records: [{
id: 1,
display_name: "Some Composer",
body: "Hello",
attachment_ids: [],
}],
},
});
},
}, function () {
QUnit.test('media dialog: upload', async function (assert) {
assert.expect(4);
const onAttachmentChangeTriggered = testUtils.makeTestPromise();
patchWithCleanup(HtmlField.prototype, {
'_onAttachmentChange': function (event) {
this._super(event);
onAttachmentChangeTriggered.resolve(true);
}
});
const defFileSelector = testUtils.makeTestPromise();
const onChangeTriggered = testUtils.makeTestPromise();
patchWithCleanup(FileSelectorControlPanel.prototype, {
setup() {
this._super();
useEffect(() => {
defFileSelector.resolve(true);
}, () => []);
},
async onChangeFileInput() {
this._super();
onChangeTriggered.resolve(true);
}
});
// create and load form view
const serviceRegistry = registry.category("services");
serviceRegistry.add("upload", uploadService);
serviceRegistry.add("unsplash", unsplashService);
const serverData = {
models: this.data,
};
serverData.actions = {
1: {
id: 1,
name: "test",
res_model: "mail.compose.message",
type: "ir.actions.act_window",
views: [[false, "form"]],
},
};
serverData.views = {
"mail.compose.message,false,search": "<search></search>",
"mail.compose.message,false,form": `
<form>
<field name="body" type="html"/>
<field name="attachment_ids" widget="many2many_binary"/>
</form>`,
};
const mockRPC = (route, args) => {
if (route === "/web_editor/attachment/add_data") {
return Promise.resolve({"id": 5, "name": "test.jpg", "description": false, "mimetype": "image/jpeg", "checksum": "7951a43bbfb08fd742224ada280913d1897b89ab",
"url": false, "type": "binary", "res_id": 1, "res_model": "note.note", "public": false, "access_token": false,
"image_src": "/web/image/1-a0e63e61/test.jpg", "image_width": 1, "image_height": 1, "original_id": false
});
}
else if (route === "/web/dataset/call_kw/ir.attachment/generate_access_token") {
return Promise.resolve(["129a52e1-6bf2-470a-830e-8e368b022e13"]);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 1);
//trigger wysiwyg mediadialog
const fixture = getFixture();
const formField = fixture.querySelector('.o_field_html[name="body"]');
const textInput = formField.querySelector('.note-editable p');
textInput.innerText = "test";
const pText = $(textInput).contents()[0];
Wysiwyg.setRange(pText, 1, pText, 2);
await new Promise((resolve) => setTimeout(resolve)); //ensure fully set up
const wysiwyg = $(textInput.parentElement).data('wysiwyg');
wysiwyg.openMediaDialog();
assert.ok(await Promise.race([defFileSelector, new Promise((res, _) => setTimeout(() => res(false), 400))]), "File Selector did not mount");
// upload test
const fileInputs = document.querySelectorAll(".o_select_media_dialog input.d-none.o_file_input");
const fileB64 = '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigD//2Q==';
const fileBytes = new Uint8Array(atob(fileB64).split('').map(char => char.charCodeAt(0)));
// redefine 'files' so we can put mock data in through js
fileInputs.forEach((input) => Object.defineProperty(input, 'files', {
value: [new File(fileBytes, "test.jpg", { type: 'image/jpeg' })],
}));
fileInputs.forEach(input => {
input.dispatchEvent(new Event('change', {}));
});
assert.ok(await Promise.race([onChangeTriggered, new Promise((res, _) => setTimeout(() => res(false), 400))]),
"File change event was not triggered");
assert.ok(await Promise.race([onAttachmentChangeTriggered, new Promise((res, _) => setTimeout(() => res(false), 400))]),
"_onAttachmentChange was not called with the new attachment, necessary for unsused upload cleanup on backend");
// wait to check that dom is properly updated
await new Promise((res, _) => setTimeout(() => res(false), 400));
assert.ok(fixture.querySelector('.o_attachment[title="test.jpg"]'));
});
});

View file

@ -1,7 +1,4 @@
odoo.define('test_website.custom_snippets', function (require) {
'use strict';
const wTourUtils = require('website.tour_utils');
import { insertSnippet, registerWebsitePreviewTour } from "@website/js/tours/tour_utils";
/**
* The purpose of this tour is to check the custom snippets flow:
@ -11,8 +8,8 @@ const wTourUtils = require('website.tour_utils');
* -> customize banner (set text)
* -> save banner as custom snippet
* -> confirm save
* -> ensure custom snippet is available
* -> drag custom snippet
* -> ensure custom snippet is available in the "add snippet" dialog
* -> add custom snippet into the page
* -> ensure block appears as banner
* -> ensure block appears as custom banner
* -> rename custom banner
@ -22,87 +19,89 @@ const wTourUtils = require('website.tour_utils');
* -> ensure it was deleted
*/
wTourUtils.registerWebsitePreviewTour('test_custom_snippet', {
registerWebsitePreviewTour('test_custom_snippet', {
url: '/',
edition: true,
test: true,
}, [
}, () => [
...insertSnippet({
id: 's_banner',
name: 'Banner',
groupName: "Intro",
}),
{
content: "drop a snippet",
trigger: ".oe_snippet[name='Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
moveTrigger: ".oe_drop_zone",
run: "drag_and_drop iframe #wrap",
content: "Customize snippet",
trigger: ":iframe #wrapwrap .s_banner h1",
run: "editor Test",
},
{
content: "customize snippet",
trigger: "iframe #wrapwrap .s_banner h1",
run: "text",
consumeEvent: "input",
content: "Save custom snippet",
trigger: "div[data-container-title='Banner'] .oe_snippet_save",
run: "click",
},
{
content: "save custom snippet",
trigger: ".snippet-option-SnippetSave we-button",
content: "Confirm reload",
trigger: ".modal-dialog button:contains('Save')",
run: "click",
},
{
content: "confirm reload",
trigger: ".modal-dialog button span:contains('Save and Reload')",
content: "Click on the block tab",
trigger: ".o-snippets-tabs button[data-name='blocks']",
run: "click",
},
{
content: "ensure custom snippet appeared",
trigger: "#oe_snippets.o_loaded .oe_snippet[name='Custom Banner']",
run: function () {
$("#oe_snippets .oe_snippet[name='Custom Banner'] .o_rename_btn").attr("style", "display: block;");
// hover is needed for rename button to appear
},
content: "Click on the Custom category block",
trigger: ".o_block_tab:not(.o_we_ongoing_insertion) #snippet_groups .o_snippet[name='Custom'].o_draggable .o_snippet_thumbnail .o_snippet_thumbnail_area",
run: "click",
},
{
content: "rename custom snippet",
trigger: ".oe_snippet[name='Custom Banner'] we-button.o_rename_btn",
extra_trigger: ".oe_snippet[name='Custom Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
content: "Ensure custom snippet preview appeared in the dialog",
trigger: ":iframe .o_snippet_preview_wrap[data-snippet-id^='s_banner_'] section[data-name='Custom Banner']",
},
{
content: "set name",
trigger: ".oe_snippet[name='Custom Banner'] input",
run: "text Bruce Banner",
content: "Rename custom snippet",
trigger: ":iframe .o_custom_snippet_edit > button",
run: "click",
},
{
content: "confirm rename",
trigger: ".oe_snippet[name='Custom Banner'] we-button.o_we_confirm_btn",
content: "Set name",
trigger: ".modal-dialog:not(.o_inactive_modal body) input[id='inputConfirmation']",
run: "edit Bruce Banner",
},
{
content: "drop custom snippet",
trigger: ".oe_snippet[name='Bruce Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
extra_trigger: "iframe body.editor_enable",
moveTrigger: ".oe_drop_zone",
run: "drag_and_drop iframe #wrap",
content: "Confirm rename",
trigger: ".modal-dialog:not(.o_inactive_modal body) footer .btn-primary",
run: "click",
},
{
content: "ensure banner section exists",
trigger: "iframe #wrap section[data-name='Banner']",
run: function () {}, // check
content: "Click on the 'Bruce Banner' snippet",
trigger: ":iframe .o_snippet_preview_wrap[data-snippet-id^='s_banner_']:has(section[data-name='Bruce Banner'])",
run: "click",
},
{
content: "ensure custom banner section exists",
trigger: "iframe #wrap section[data-name='Bruce Banner']",
run: function () {
$("#oe_snippets .oe_snippet[name='Bruce Banner'] .o_delete_btn").attr("style", "display: block;");
// hover is needed for delete button to appear
},
content: "Ensure banner section exists",
trigger: ":iframe #wrap section[data-name='Banner']",
},
{
content: "delete custom snippet",
trigger: ".oe_snippet[name='Bruce Banner'] we-button.o_delete_btn",
extra_trigger: ".oe_snippet[name='Bruce Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
content: "Ensure custom banner section exists",
trigger: ":iframe #wrap section[data-name='Bruce Banner']",
},
{
content: "confirm delete",
trigger: ".modal-dialog button:has(span:contains('Yes'))",
content: "Click on the Custom category block",
trigger: ".o_block_tab:not(.o_we_ongoing_insertion) #snippet_groups .o_snippet[name='Custom'].o_draggable .o_snippet_thumbnail .o_snippet_thumbnail_area",
run: "click",
},
{
content: "ensure custom snippet disappeared",
trigger: "#oe_snippets:not(:has(.oe_snippet[name='Bruce Banner']))",
run: function () {}, // check
content: "Delete custom snippet",
trigger: ":iframe .o_custom_snippet_edit > button + button",
run: "click",
},
{
content: "Confirm delete",
trigger: ".modal-dialog button:contains('Yes')",
run: "click",
},
{
content: "Ensure custom snippet disappeared",
trigger: ":iframe .o_add_snippets_preview:not(:has(section[data-name='Bruce Banner']))",
},
]);
});

View file

@ -1,56 +1,81 @@
odoo.define('test_website.error_views', function (require) {
'use strict';
import { registry } from "@web/core/registry";
var tour = require('web_tour.tour');
tour.register('test_error_website', {
test: true,
registry.category("web_tour.tours").add('test_error_website', {
url: '/test_error_view',
},
[
steps: () => [
// RPC ERROR
{
content: "trigger rpc user error",
trigger: 'a[href="/test_user_error_json"]',
}, {
run: "click",
},
{
trigger: '.o_notification:contains("This is a user rpc test")',
},
{
content: "rpc user error modal has message",
extra_trigger: 'div.o_notification_content:contains("This is a user rpc test")',
trigger: 'button.o_notification_close',
run: "click",
}, {
content: "trigger rpc access error",
trigger: 'a[href="/test_access_error_json"]',
}, {
run: "click",
},
{
trigger: '.o_notification:contains("This is an access rpc test")',
},
{
content: "rpc access error modal has message",
extra_trigger: 'div.o_notification_content:contains("This is an access rpc test")',
trigger: 'button.o_notification_close',
run: "click",
}, {
content: "trigger validation rpc error",
trigger: 'a[href="/test_validation_error_json"]',
}, {
run: "click",
},
{
trigger: '.o_notification:contains("This is a validation rpc test")',
},
{
content: "rpc validation error modal has message",
extra_trigger: 'div.o_notification_content:contains("This is a validation rpc test")',
trigger: 'button.o_notification_close',
run: "click",
}, {
content: "trigger rpc missing error",
trigger: 'a[href="/test_missing_error_json"]',
}, {
run: "click",
},
{
trigger: '.o_notification:contains("This is a missing rpc test")',
},
{
content: "rpc missing error modal has message",
extra_trigger: 'div.o_notification_content:contains("This is a missing rpc test")',
trigger: 'button.o_notification_close',
run: "click",
}, {
content: "trigger rpc error 403",
trigger: 'a[href="/test_access_denied_json"]',
}, {
run: "click",
},
{
trigger: '.o_notification:contains("This is an access denied rpc test")',
},
{
content: "rpc error 403 modal has message",
extra_trigger: 'div.o_notification_content:contains("This is an access denied rpc test")',
trigger: 'button.o_notification_close',
run: "click",
}, {
content: "trigger rpc error 500",
trigger: 'a[href="/test_internal_error_json"]',
}, {
run: "click",
},
{
trigger: "div.o_error_dialog.modal-content",
},
{
content: "rpc error 500 modal is an ErrorDialog",
extra_trigger: 'div.o_dialog_error.modal-content div.alert.alert-warning',
trigger: '.modal-footer button.btn.btn-primary',
run: "click",
},
// HTTP ERROR
{
@ -59,94 +84,139 @@ tour.register('test_error_website', {
run: function () {
window.location.href = window.location.origin + '/test_user_error_http?debug=0';
},
}, {
expectUnloadPage: true,
},
{
trigger: 'h1:contains("Something went wrong.")',
},
{
content: "http user error page has title and message",
extra_trigger: 'h1:contains("Something went wrong.")',
trigger: 'div.container pre:contains("This is a user http test")',
run: function () {
window.location.href = window.location.origin + '/test_user_error_http?debug=1';
},
}, {
expectUnloadPage: true,
},
{
trigger: 'h1:contains("Something went wrong.")',
},
{
content: "http user error page debug has title and message open",
extra_trigger: 'h1:contains("Something went wrong.")',
trigger: 'div#error_main.collapse.show pre:contains("This is a user http test")',
run: function () {},
}, {
content: "http user error page debug has traceback closed",
trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
run: function () {
window.location.href = window.location.origin + '/test_validation_error_http?debug=0';
},
}, {
expectUnloadPage: true,
},
{
trigger: 'h1:contains("Something went wrong.")',
},
{
content: "http validation error page has title and message",
extra_trigger: 'h1:contains("Something went wrong.")',
trigger: 'div.container pre:contains("This is a validation http test")',
run: function () {
window.location.href = window.location.origin + '/test_validation_error_http?debug=1';
},
}, {
expectUnloadPage: true,
},
{
trigger: 'h1:contains("Something went wrong.")',
},
{
content: "http validation error page debug has title and message open",
extra_trigger: 'h1:contains("Something went wrong.")',
trigger: 'div#error_main.collapse.show pre:contains("This is a validation http test")',
run: function () {},
}, {
content: "http validation error page debug has traceback closed",
trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
run: function () {
window.location.href = window.location.origin + '/test_access_error_http?debug=0';
},
}, {
expectUnloadPage: true,
},
{
trigger: 'h1:contains("403: Forbidden")',
},
{
content: "http access error page has title and message",
extra_trigger: 'h1:contains("403: Forbidden")',
trigger: 'div.container pre:contains("This is an access http test")',
run: function () {
window.location.href = window.location.origin + '/test_access_error_http?debug=1';
},
}, {
expectUnloadPage: true,
},
{
trigger: 'h1:contains("403: Forbidden")',
},
{
content: "http access error page debug has title and message open",
extra_trigger: 'h1:contains("403: Forbidden")',
trigger: 'div#error_main.collapse.show pre:contains("This is an access http test")',
run: function () {},
}, {
content: "http access error page debug has traceback closed",
trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
run: function () {
window.location.href = window.location.origin + '/test_view_access_error?debug=0';
},
expectUnloadPage: true,
},
{
trigger: 'h1:contains("403: Forbidden")',
},
{
content: "http access error page has title and message",
trigger: 'div.container pre:contains("Uh-oh! Looks like you have stumbled upon some top-secret records.")',
run: function () {
window.location.href = window.location.origin + '/test_view_access_error?debug=1';
},
expectUnloadPage: true,
},
{
trigger: 'h1:contains("403: Forbidden")',
},
{
content: "http access error page debug has title and message open",
trigger: 'div#error_main.collapse.show pre:contains("Uh-oh! Looks like you have stumbled upon some top-secret records.")',
}, {
content: "http access error page debug has traceback closed",
trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
run: function () {
window.location.href = window.location.origin + '/test_missing_error_http?debug=0';
},
}, {
content: "http missing error page has title and message",
extra_trigger: 'h1:contains("Something went wrong.")',
trigger: 'div.container pre:contains("This is a missing http test")',
expectUnloadPage: true,
},
{
trigger: 'h1:contains("Error 404")',
run: function () {
window.location.href = window.location.origin + '/test_missing_error_http?debug=1';
},
}, {
content: "http missing error page debug has title and message open",
extra_trigger: 'h1:contains("Something went wrong.")',
trigger: 'div#error_main.collapse.show pre:contains("This is a missing http test")',
run: function () {},
}, {
content: "http missing error page debug has traceback closed",
trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
run: function () {
window.location.href = window.location.origin + '/test_access_denied_http?debug=0';
},
}, {
content: "http error 403 page has title but no message",
extra_trigger: 'h1:contains("403: Forbidden")',
trigger: 'div#wrap:not(:has(pre:contains("This is an access denied http test"))', //See ir_http.py handle_exception, the exception is replaced so there is no message !
expectUnloadPage: true,
},
{
trigger: 'h1:contains("Error 404")',
run: function () {
window.location.href = window.location.origin + '/test_access_denied_http?debug=1';
},
}, {
content: "http 403 error page debug has title but no message",
extra_trigger: 'h1:contains("403: Forbidden")',
trigger: 'div#debug_infos:not(:has(#error_main))',
run: function () {},
}, {
content: "http 403 error page debug has traceback open",
trigger: 'body:has(div#error_traceback.collapse.show pre#exception_traceback)',
run: function () {},
expectUnloadPage: true,
},
]);
});
{
trigger: 'h1:contains("403: Forbidden")',
},
{
content: "http error 403 page has title but no message",
// See http.py _transactionning, the exception is replaced so there is no message !
trigger: 'div#wrap:not(:has(pre:contains("Traceback"))',
run: function () {
window.location.href = window.location.origin + '/test_access_denied_http?debug=1';
},
expectUnloadPage: true,
},
{
trigger: 'h1:contains("403: Forbidden")',
},
{
content: "http 403 error page debug has title but no message",
trigger: 'div#wrap:not(:has(pre:contains("Traceback"))',
},
]});

View file

@ -0,0 +1,59 @@
import {
clickOnEditAndWaitEditMode,
clickOnSave,
registerWebsitePreviewTour,
changeOptionInPopover,
} from "@website/js/tours/tour_utils";
registerWebsitePreviewTour(
"test_form_conditional_visibility_record_field",
{
url: "/test_website/model_item/1",
edition: true,
},
() => [
{
content: "Select name field",
trigger: ":iframe .s_website_form .s_website_form_input[name=name]",
run: "click",
},
...changeOptionInPopover("Field", "Visibility", "Visible only if"),
{
content: "Open model selector",
trigger: "button[id='hidden_condition_record_opt']:contains('Test Tag')",
run: "click",
},
{
content: "Set model to tag #2",
trigger: ".o_popover div.o-dropdown-item:contains('Test Tag #2')",
run: "click",
},
...clickOnSave(),
{
content: "Name field is hidden",
trigger: ":iframe .s_website_form:has(.s_website_form_field_hidden_if.d-none)",
},
...clickOnEditAndWaitEditMode(),
{
content: "Select name field",
trigger: ":iframe .s_website_form .s_website_form_input[name=name]",
run: "click",
},
{
content: "Open comparator dropdown",
trigger: "button[id='hidden_condition_record_opt']:contains('Is equal to')",
run: "click",
},
{
content: "Set comparator to Is not equal",
trigger: ".o_popover div.o-dropdown-item:contains('Is not equal to')",
run: "click",
},
...clickOnSave(),
{
content: "Name field is shown",
trigger: ":iframe .s_website_form:has(.s_website_form_field_hidden_if:not(.d-none))",
},
],
);

View file

@ -1,6 +1,4 @@
/** @odoo-module **/
import wTourUtils from 'website.tour_utils';
import { insertSnippet, registerWebsitePreviewTour } from '@website/js/tours/tour_utils';
/**
* The purpose of this tour is to check the link on image flow.
@ -8,78 +6,81 @@ import wTourUtils from 'website.tour_utils';
const selectImageSteps = [{
content: "select block",
trigger: "iframe #wrapwrap .s_text_image",
trigger: ":iframe #wrapwrap .s_text_image",
async run(helpers) {
await helpers.click();
const el = this.anchor;
const sel = el.ownerDocument.getSelection();
sel.collapse(el, 0);
el.focus();
},
}, {
content: "check link popover disappeared",
trigger: "iframe body:not(:has(.o_edit_menu_popover))",
run: () => {}, // check
trigger: ":iframe body:not(:has(.o_edit_menu_popover))",
}, {
content: "select image",
trigger: "iframe #wrapwrap .s_text_image img",
trigger: ":iframe #wrapwrap .s_text_image img",
run: "click",
}];
wTourUtils.registerWebsitePreviewTour('test_image_link', {
test: true,
registerWebsitePreviewTour('test_image_link', {
url: '/',
edition: true,
}, [
wTourUtils.dragNDrop({
}, () => [
...insertSnippet({
id: 's_text_image',
name: 'Text - Image',
groupName: "Content",
}),
...selectImageSteps,
{
content: "enable link",
trigger: "#oe_snippets we-customizeblock-options:has(we-title:contains('Image')) we-customizeblock-option:has(we-title:contains(Media)) we-button.fa-link",
trigger: ".o_customize_tab [data-container-title='Image'] button[data-action-id='setLink']",
run: "click",
}, {
content: "enter site URL",
trigger: "#oe_snippets we-customizeblock-options:has(we-title:contains('Image')) we-input:contains(Your URL) input",
run: "text odoo.com",
trigger: ".o_customize_tab [data-container-title='Image'] div[data-action-id='setUrl'] input",
run: "edit odoo.com && click body",
},
...selectImageSteps,
{
content: "check popover content has site URL",
trigger: "iframe .o_edit_menu_popover a.o_we_url_link[href='http://odoo.com/']:contains(http://odoo.com/)",
run: () => {}, // check
trigger: ".o-we-linkpopover a.o_we_url_link[href='http://odoo.com']:contains(http://odoo.com)",
}, {
content: "remove URL",
trigger: "#oe_snippets we-customizeblock-options:has(we-title:contains('Image')) we-input:contains(Your URL) input",
run: "remove_text",
trigger: ".o_customize_tab [data-container-title='Image'] div[data-action-id='setUrl'] input",
run: "clear && click body",
},
...selectImageSteps,
{
content: "check popover content has no URL",
trigger: "iframe .o_edit_menu_popover a.o_we_url_link:not([href]):contains(No URL specified)",
run: () => {}, // check
trigger: ".o-we-linkpopover .o_we_href_input_link:value()",
}, {
content: "enter email URL",
trigger: "#oe_snippets we-customizeblock-options:has(we-title:contains('Image')) we-input:contains(Your URL) input",
run: "text mailto:test@test.com",
trigger: ".o_customize_tab [data-container-title='Image'] div[data-action-id='setUrl'] input",
run: "edit mailto:test@test.com && click body",
},
...selectImageSteps,
{
content: "check popover content has mail URL",
trigger: "iframe .o_edit_menu_popover:has(.fa-envelope-o) a.o_we_url_link[href='mailto:test@test.com']:contains(mailto:test@test.com)",
run: () => {}, // check
trigger: ".o-we-linkpopover:has(.fa-envelope-o) a.o_we_url_link[href='mailto:test@test.com']:contains(mailto:test@test.com)",
}, {
content: "enter phone URL",
trigger: "#oe_snippets we-customizeblock-options:has(we-title:contains('Image')) we-input:contains(Your URL) input",
run: "text tel:555-2368",
trigger: ".o_customize_tab [data-container-title='Image'] div[data-action-id='setUrl'] input",
run: "edit tel:555-2368 && click body",
},
...selectImageSteps,
{
content: "check popover content has phone URL",
trigger: "iframe .o_edit_menu_popover:has(.fa-phone) a.o_we_url_link[href='tel:555-2368']:contains(tel:555-2368)",
run: () => {}, // check
trigger: ".o-we-linkpopover:has(.fa-phone) a.o_we_url_link[href='tel:555-2368']:contains(tel:555-2368)",
}, {
content: "remove URL",
trigger: "#oe_snippets we-customizeblock-options:has(we-title:contains('Image')) we-input:contains(Your URL) input",
run: "remove_text",
trigger: ".o_customize_tab [data-container-title='Image'] div[data-action-id='setUrl'] input",
run: "clear && click body",
},
...selectImageSteps,
{
content: "check popover content has no URL",
trigger: "iframe .o_edit_menu_popover a.o_we_url_link:not([href]):contains(No URL specified)",
run: () => {}, // check
trigger: ".o-we-linkpopover .o_we_href_input_link:value()",
},
]);

View file

@ -1,13 +1,10 @@
odoo.define('test_website.image_upload_progress', function (require) {
'use strict';
import { insertSnippet, registerWebsitePreviewTour } from "@website/js/tours/tour_utils";
const wTourUtils = require('website.tour_utils');
const { FileSelectorControlPanel } = require('@web_editor/components/media_dialog/file_selector');
const { patch, unpatch } = require('web.utils');
import { FileSelectorControlPanel } from "@html_editor/main/media/media_dialog/file_selector";
import { patch } from "@web/core/utils/patch";
let patchWithError = false;
const patchMediaDialog = () => patch(FileSelectorControlPanel.prototype, 'test_website.mock_image_widgets', {
const patchMediaDialog = () => patch(FileSelectorControlPanel.prototype, {
async onChangeFileInput() {
const getFileFromB64 = (fileData) => {
const binary = atob(fileData[2]);
@ -37,46 +34,50 @@ const patchMediaDialog = () => patch(FileSelectorControlPanel.prototype, 'test_w
}
});
const unpatchMediaDialog = () => unpatch(FileSelectorControlPanel.prototype, 'test_website.mock_image_widgets');
let unpatchMediaDialog = null;
const setupSteps = [{
content: "reload to load patch",
trigger: ".o_website_preview",
run: () => {
patchMediaDialog();
},
}, {
content: "drop a snippet",
trigger: "#oe_snippets .oe_snippet[name='Text - Image'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
moveTrigger: "iframe .oe_drop_zone",
run: "drag_and_drop iframe #wrap",
}, {
content: "drop a snippet",
trigger: "#oe_snippets .oe_snippet[name='Image Gallery'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
extra_trigger: "body.editor_has_snippets",
moveTrigger: ".oe_drop_zone",
run: "drag_and_drop iframe #wrap",
}];
const setupSteps = function () {
return [
{
content: "reload to load patch",
trigger: ".o_website_preview",
run() {
unpatchMediaDialog = patchMediaDialog();
},
},
...insertSnippet({
id: "s_text_image",
name: "Text - Image",
groupName: "Content",
}),
...insertSnippet({
id: "s_image_gallery",
name: "Image Gallery",
groupName: "Images",
})
];
};
const formatErrorMsg = "format is not supported. Try with: .gif, .jpe, .jpeg, .jpg, .png, .svg";
const formatErrorMsg = "format is not supported. Try with: .gif, .jpe, .jpeg, .jpg, .png, .svg, .webp";
wTourUtils.registerWebsitePreviewTour('test_image_upload_progress', {
registerWebsitePreviewTour('test_image_upload_progress', {
url: '/test_image_progress',
test: true,
edition: true,
}, [
...setupSteps,
}, () => [
...setupSteps(),
// 1. Check multi image upload
{
content: "click on dropped snippet",
trigger: "iframe #wrap .s_image_gallery .img",
trigger: ":iframe #wrap .s_image_gallery .img",
run: "click",
}, {
content: "click on add images to open image dialog (in multi mode)",
trigger: 'we-customizeblock-option [data-add-images]',
trigger: "button[data-action-id='addImage']",
run: "click",
}, {
content: "manually trigger input change",
trigger: ".o_select_media_dialog .o_upload_media_button",
run: () => {
run() {
// This will trigger upload of dummy files for test purpose, as a
// test can't select local files to upload into the input.
document.body.querySelector('.o_select_media_dialog .o_file_input').dispatchEvent(new Event('change'));
@ -84,52 +85,50 @@ wTourUtils.registerWebsitePreviewTour('test_image_upload_progress', {
}, {
content: "check upload progress bar is correctly shown (1)",
trigger: `.o_we_progressbar:contains('icon.ico'):contains('${formatErrorMsg}')`,
in_modal: false,
run: function () {}, // it's a check
}, {
content: "check upload progress bar is correctly shown (2)",
trigger: `.o_we_progressbar:contains('image.webp'):contains('${formatErrorMsg}')`,
in_modal: false,
run: function () {}, // it's a check
trigger: ".o_we_progressbar:contains('image.webp'):contains('File has been uploaded')",
}, {
content: "check upload progress bar is correctly shown (3)",
trigger: ".o_we_progressbar:contains('image.png'):contains('File has been uploaded')",
in_modal: false,
run: function () {}, // it's a check
}, {
content: "check upload progress bar is correctly shown (4)",
trigger: ".o_we_progressbar:contains('image.jpeg'):contains('File has been uploaded')",
in_modal: false,
run: function () {}, // it's a check
}, {
content: "there should only have one notification toaster",
},
{
trigger: ".o_notification",
in_modal: false,
run: () => {
const notificationCount = $('.o_notification').length;
},
{
content: "there should only have one notification toaster",
trigger: "body",
run() {
const notificationCount = document.querySelectorAll(".o_notification").length;
if (notificationCount !== 1) {
console.error("There should be one noficiation toaster opened, and only one.");
throw new Error(`There should be one notification toaster opened, and only one, found ${notificationCount}.`);
}
}
}, {
content: "close notification",
trigger: '.o_notification_close',
in_modal: false,
run: "click",
}, {
content: "close media dialog",
trigger: '.modal-footer .btn-secondary',
run: "click",
},
// 2. Check success single image upload
{
content: "click on dropped snippet",
trigger: "iframe #wrap .s_text_image .img",
trigger: ":iframe #wrap .s_text_image .img",
run: "click",
}, {
content: "click on replace media to open image dialog",
trigger: 'we-customizeblock-option [data-replace-media]',
trigger: "button[data-action-id='replaceMedia']",
run: "click",
}, {
content: "manually trigger input change",
trigger: ".o_select_media_dialog .o_upload_media_button",
run: () => {
run() {
// This will trigger upload of dummy files for test purpose, as a
// test can't select local files to upload into the input.
document.body.querySelector('.o_select_media_dialog .o_file_input').dispatchEvent(new Event('change'));
@ -137,43 +136,38 @@ wTourUtils.registerWebsitePreviewTour('test_image_upload_progress', {
}, {
content: "check upload progress bar is correctly shown",
trigger: ".o_we_progressbar:contains('image.png')",
in_modal: false,
run: function () {}, // it's a check
}, {
content: "there should only have one notification toaster",
trigger: ".o_notification",
in_modal: false,
run: () => {
const notificationCount = $('.o_notification').length;
run() {
const notificationCount = document.querySelectorAll(".o_notification").length;
if (notificationCount !== 1) {
console.error("There should be one noficiation toaster opened, and only one.");
throw new Error(`There should be one notification toaster opened, and only one, found ${notificationCount}.`);
}
}
}, {
content: "media dialog has closed after the upload",
trigger: 'body:not(:has(.o_select_media_dialog))',
run: () => {}, // It's a check.
}, {
content: "the upload progress toast was updated",
trigger: ".o_we_progressbar:contains('image.png'):contains('File has been uploaded')",
run: () => {}, // It's a check.
}, {
content: "toaster should disappear after a few seconds if the uploaded image is successful",
trigger: "body:not(:has(.o_we_progressbar))",
run: function () {}, // it's a check
},
// 3. Check error single image upload
{
content: "click on dropped snippet",
trigger: "iframe #wrap .s_text_image .img",
trigger: ":iframe #wrap .s_text_image .img",
run: "click",
}, {
content: "click on replace media to open image dialog",
trigger: 'we-customizeblock-option [data-replace-media]',
trigger: "button[data-action-id='replaceMedia']",
run: "click",
}, {
content: "manually trigger input change",
trigger: ".o_select_media_dialog .o_upload_media_button",
in_modal: false,
run: () => {
run() {
patchWithError = true;
// This will trigger upload of dummy files for test purpose, as a
// test can't select local files to upload into the input.
@ -183,18 +177,16 @@ wTourUtils.registerWebsitePreviewTour('test_image_upload_progress', {
}, {
content: "check upload progress bar is correctly shown",
trigger: `.o_we_progressbar:contains('icon.ico'):contains('${formatErrorMsg}')`,
in_modal: false,
run: function () {
run() {
patchWithError = false;
},
}, {
content: "there should only have one notification toaster",
trigger: ".o_notification",
in_modal: false,
run: () => {
const notificationCount = $('.o_notification').length;
run() {
const notificationCount = document.querySelectorAll(".o_notification").length;
if (notificationCount !== 1) {
console.error("There should be one noficiation toaster opened, and only one.");
throw new Error(`There should be one noficiation toaster opened, and only one, found ${notificationCount}.`);
}
unpatchMediaDialog();
}
@ -202,44 +194,45 @@ wTourUtils.registerWebsitePreviewTour('test_image_upload_progress', {
]);
wTourUtils.registerWebsitePreviewTour('test_image_upload_progress_unsplash', {
registerWebsitePreviewTour('test_image_upload_progress_unsplash', {
url: '/test_image_progress',
test: true,
edition: true,
}, [
...setupSteps,
}, () => [
...setupSteps(),
// 1. Check multi image upload
{
content: "click on dropped snippet",
trigger: "iframe #wrap .s_image_gallery .img",
trigger: ":iframe #wrap .s_image_gallery .img",
run: "click",
}, {
content: "click on replace media to open image dialog",
trigger: 'we-customizeblock-option [data-replace-media]',
trigger: "button[data-action-id='replaceMedia']",
run: "click",
}, {
content: "search 'fox' images",
trigger: ".o_we_search",
run: "text fox",
run: "edit fox",
}, {
content: "click on unsplash result", // note that unsplash is mocked
trigger: "img[alt~=fox]"
}, {
trigger: ".o_we_media_dialog_img_wrapper:has(img[alt~=fox]) + .o_button_area",
run: "click",
},
{
trigger: ".o_notification_close",
},
{
content: "check that the upload progress bar is correctly shown",
// ensure it is there so we are sure next step actually test something
extra_trigger: '.o_notification_close',
trigger: ".o_we_progressbar:contains('fox'):contains('File has been uploaded')",
in_modal: false,
run: function () {}, // it's a check
}, {
content: "notification should close after 3 seconds",
trigger: 'body:not(:has(.o_notification_close))',
in_modal: false,
run: "click",
}, {
content: "unsplash image (mocked to logo) should have been used",
trigger: "iframe #wrap .s_image_gallery .img[data-original-src^='/unsplash/HQqIOc8oYro/fox']",
run: () => {
trigger: ":iframe #wrap .s_image_gallery img[data-original-src^='/unsplash/HQqIOc8oYro/fox']",
run() {
unpatchMediaDialog();
},
},
]);
});

View file

@ -1,16 +1,12 @@
odoo.define('test_website.json_auth', function (require) {
'use strict';
import { registry } from "@web/core/registry";
import { rpc } from "@web/core/network/rpc";
var tour = require('web_tour.tour');
var session = require('web.session')
tour.register('test_json_auth', {
test: true,
}, [{
registry.category("web_tour.tours").add('test_json_auth', {
steps: () => [{
trigger: 'body',
run: async function () {
await session.rpc('/test_get_dbname').then( function (result){
return session.rpc("/web/session/authenticate", {
await rpc('/test_get_dbname').then( function (result){
return rpc("/web/session/authenticate", {
db: result,
login: 'admin',
password: 'admin'
@ -18,9 +14,8 @@ tour.register('test_json_auth', {
});
window.location.href = window.location.origin;
},
expectUnloadPage: true,
}, {
trigger: 'span:contains(Mitchell Admin), span:contains(Administrator)',
run: function () {},
}
]);
});
]});

View file

@ -1,77 +1,87 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import tour from 'web_tour.tour';
tour.register('test_website_page_manager', {
test: true,
url: '/web#action=test_website.action_test_model_multi_website',
}, [
registry.category("web_tour.tours").add('test_website_page_manager', {
url: '/odoo/action-test_website.action_test_model_multi_website',
steps: () => [
// Part 1: check that the website filter is working
{
content: "Check that we see records from My Website",
trigger: ".o_list_table .o_data_row .o_data_cell[name=name]:contains('Test Multi Model Website 1') " +
"~ .o_data_cell[name=website_id]:contains('My Website')",
run: () => null, // it's a check
}, {
content: "Check that there is only 2 records in the pager",
trigger: ".o_pager .o_pager_value:contains('1-2')",
run: () => null, // it's a check
}, {
content: "Click on the 'Select all records' checkbox",
trigger: "thead .o_list_record_selector",
run: "click",
}, {
content: "Check that there is only 2 records selected",
trigger: ".o_list_selection_box:contains('2 selected')",
run: () => null, // it's a check
trigger: ".o_selection_box:contains(2):contains(selected)",
}, {
content: "Click on My Website search filter",
trigger: "button.dropdown-toggle:contains('My Website')",
content: "Click on the 'Select all records' checkbox again to unselect all records and see the search bar",
trigger: "thead .o_list_record_selector",
run: "click",
}, {
content: "Select My Website 2",
trigger: ".dropdown-menu.show > .dropdown-item:contains('My Website 2')",
content: "Click on the search options",
trigger: ".o_searchview_dropdown_toggler",
run: "click",
}, {
content: "Remove 'My Website' filter",
trigger: ".o_filter_menu .o-dropdown-item:contains('My Website')",
run: "click",
}, {
content: "Select 'My Website 2' filter",
trigger: ".o_filter_menu .o-dropdown-item:contains('My Website 2')",
run: "click",
}, {
// This step is just here to ensure there is more records than the 2
// available on website 1, to ensure the test is actually testing something.
content: "Check that we see records from My Website 2",
trigger: ".o_list_table .o_data_row .o_data_cell[name=name]:contains('Test Model Multi Website 2') " +
"~ .o_data_cell[name=website_id]:contains('My Website 2')",
run: () => null, // it's a check
},
// Part 2: ensure Kanban View is working / not crashing
{
content: "Click on Kanban View",
trigger: '.o_cp_switch_buttons .o_kanban',
}, {
run: "click",
},
{
trigger: ".o_kanban_renderer",
},
{
content: "Click on List View",
extra_trigger: '.o_kanban_renderer',
trigger: '.o_cp_switch_buttons .o_list',
run: "click",
}, {
content: "Wait for List View to be loaded",
trigger: '.o_list_renderer',
run: () => null, // it's a check
}]);
}]
});
tour.register('test_website_page_manager_js_class_bug', {
test: true,
url: '/web#action=test_website.action_test_model_multi_website_js_class_bug',
}, [{
registry.category("web_tour.tours").add('test_website_page_manager_js_class_bug', {
url: '/odoo/action-test_website.action_test_model_multi_website_js_class_bug',
steps: () => [
{
content: "Click on Kanban View",
trigger: '.o_cp_switch_buttons .o_kanban',
run: "click",
}, {
content: "Wait for Kanban View to be loaded",
trigger: '.o_kanban_renderer',
run: () => null, // it's a check
}]);
}]
});
tour.register('test_website_page_manager_no_website_id', {
test: true,
url: '/web#action=test_website.action_test_model',
}, [{
registry.category("web_tour.tours").add('test_website_page_manager_no_website_id', {
url: '/odoo/action-test_website.action_test_model',
steps: () => [
{
content: "Click on Kanban View",
trigger: '.o_cp_switch_buttons .o_kanban',
run: "click",
}, {
content: "Wait for Kanban View to be loaded",
trigger: '.o_kanban_renderer',
run: () => null, // it's a check
}]);
}]
});

View file

@ -1,19 +1,20 @@
/** @odoo-module **/
import { patch } from '@web/core/utils/patch';
import { VideoSelector } from '@web_editor/components/media_dialog/video_selector';
import wTourUtils from 'website.tour_utils';
import { patch } from "@web/core/utils/patch";
import { VideoSelector } from "@html_editor/main/media/media_dialog/video_selector";
import {
changeOption,
insertSnippet,
registerWebsitePreviewTour,
} from '@website/js/tours/tour_utils';
const VIDEO_URL = 'https://www.youtube.com/watch?v=Dpq87YCHmJc';
/**
* The purpose of this tour is to check the media replacement flow.
*/
wTourUtils.registerWebsitePreviewTour('test_replace_media', {
registerWebsitePreviewTour('test_replace_media', {
url: '/',
test: true,
edition: true,
}, [
}, () => [
{
trigger: "body",
run: function () {
@ -23,138 +24,148 @@ wTourUtils.registerWebsitePreviewTour('test_replace_media', {
// specific to an URL only, it is acceptable).
// TODO if we ever give the possibility to upload its own videos,
// this won't be necessary anymore.
patch(VideoSelector.prototype, "Video selector patch", {
patch(VideoSelector.prototype, {
async _getVideoURLData(src, options) {
if (src === VIDEO_URL || src === 'about:blank') {
return {platform: 'youtube', embed_url: 'about:blank'};
}
return this._super(...arguments);
return super._getVideoURLData(...arguments);
},
});
},
},
{
content: "drop picture snippet",
trigger: "#oe_snippets .oe_snippet[name='Picture'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
moveTrigger: "iframe .oe_drop_zone",
run: "drag_and_drop iframe #wrap",
},
...insertSnippet({
name: 'Title - Image',
id: 's_picture',
groupName: "Images",
}),
{
content: "select image",
trigger: "iframe .s_picture figure img",
trigger: ":iframe .s_picture figure img",
run: "click",
},
{
content: "ensure image size is displayed",
trigger: "#oe_snippets we-title:contains('Image') .o_we_image_weight:contains('kb')",
run: function () {}, // check
trigger: ".o_customize_tab [data-container-title='Image'] .options-container-header:contains('kb')",
},
wTourUtils.changeOption("ImageTools", 'we-select[data-name="shape_img_opt"] we-toggler'),
wTourUtils.changeOption("ImageTools", "we-button[data-set-img-shape]"),
changeOption("Image", "[data-label='Shape'] .dropdown-toggle"),
{
content: "replace image",
trigger: "#oe_snippets we-button[data-replace-media]",
content: "Click on the first image shape",
trigger: "button[data-action-id='setImageShape']",
run: "click",
},
{
content: "Open MediaDialog from an image",
trigger: "button[data-action-id='replaceMedia']",
run: "click",
},
{
content: "select svg",
trigger: ".o_select_media_dialog img[title='sample.svg']",
trigger: ".o_select_media_dialog .o_button_area[aria-label='sample.svg']",
run: "click",
},
{
content: "ensure the svg doesn't have a shape",
trigger: "iframe .s_picture figure img:not([data-shape])",
run: function () {}, // check
content: "ensure the svg does have a shape",
trigger: ":iframe .s_picture figure img[data-shape]",
},
{
content: "ensure image size is not displayed",
trigger: "#oe_snippets we-title:contains('Image'):not(:has(.o_we_image_weight:visible))",
run: function () {}, // check
content: "ensure image size is displayed",
trigger: ".o_customize_tab [data-container-title='Image'] span[title='Size']",
},
{
content: "replace image",
trigger: "#oe_snippets we-button[data-replace-media]",
trigger: "button[data-action-id='replaceMedia']",
run: "click",
},
{
content: "go to pictogram tab",
trigger: ".o_select_media_dialog .nav-link:contains('Icons')",
run: "click",
},
{
content: "select an icon",
trigger: ".o_select_media_dialog:has(.nav-link.active:contains('Icons')) .tab-content span.fa-lemon-o",
trigger: ".o_select_media_dialog:has(.nav-link.active:contains('Icons')) .tab-content span.fa-heart",
run: "click",
},
{
content: "ensure icon block is displayed",
trigger: "#oe_snippets we-customizeblock-options we-title:contains('Icon')",
run: function () {}, // check
trigger: ".o_customize_tab [data-container-title='Icon']",
},
{
content: "select footer",
trigger: "iframe footer",
trigger: ":iframe footer",
run: "click",
},
{
content: "select icon",
trigger: "iframe .s_picture figure span.fa-lemon-o",
trigger: ":iframe .s_picture figure span.fa-heart",
run: "click",
},
{
content: "ensure icon block is still displayed",
trigger: "#oe_snippets we-customizeblock-options we-title:contains('Icon')",
run: function () {}, // check
trigger: ".o_customize_tab [data-container-title='Icon']",
},
{
content: "replace icon",
trigger: "#oe_snippets we-button[data-replace-media]",
trigger: "button[data-action-id='replaceMedia']",
run: "click",
},
{
content: "go to video tab",
trigger: ".o_select_media_dialog .nav-link:contains('Video')",
run: "click",
},
{
content: "enter a video URL",
trigger: ".o_select_media_dialog #o_video_text",
// Design your first web page.
run: `text ${VIDEO_URL}`,
run: `edit ${VIDEO_URL}`,
},
{
content: "wait for preview to appear",
// "about:blank" because the VideoWidget was patched at the start of this tour
trigger: ".o_select_media_dialog div.media_iframe_video iframe[src='about:blank']",
run: function () {}, // check
trigger: ".o_select_media_dialog div.media_iframe_video [src='about:blank']:iframe body",
},
{
content: "confirm selection",
trigger: ".o_select_media_dialog .modal-footer .btn-primary",
run: "click",
},
{
content: "ensure video option block is displayed",
trigger: "#oe_snippets we-customizeblock-options we-title:contains('Video')",
run: function () {}, // check
trigger: ".o_customize_tab [data-container-title='Video']",
},
{
content: "replace image",
trigger: "#oe_snippets we-button[data-replace-media]",
trigger: ".btn-success[data-action-id='replaceMedia']",
run: "click",
},
{
content: "go to pictogram tab",
trigger: ".o_select_media_dialog .nav-link:contains('Icons')",
run: "click",
},
{
content: "select an icon",
trigger: ".o_select_media_dialog:has(.nav-link.active:contains('Icons')) .tab-content span.fa-lemon-o",
trigger: ".o_select_media_dialog:has(.nav-link.active:contains('Icons')) .tab-content span.fa-heart",
run: "click",
},
{
content: "ensure icon block is displayed",
trigger: "#oe_snippets we-customizeblock-options we-title:contains('Icon')",
run: function () {}, // check
trigger: ".o_customize_tab [data-container-title='Icon']",
},
{
content: "select footer",
trigger: "iframe footer",
trigger: ":iframe footer",
run: "click",
},
{
content: "select icon",
trigger: "iframe .s_picture figure span.fa-lemon-o",
trigger: ":iframe .s_picture figure span.fa-heart",
run: "click",
},
{
content: "ensure icon block is still displayed",
trigger: "#oe_snippets we-customizeblock-options we-title:contains('Icon')",
run: function () {}, // check
trigger: ".o_customize_tab [data-container-title='Icon']",
},
]);

View file

@ -1,8 +1,6 @@
/* global ace */
odoo.define('test_website.reset_views', function (require) {
'use strict';
const wTourUtils = require('website.tour_utils');
import { clickOnSave, registerWebsitePreviewTour } from "@website/js/tours/tour_utils";
var BROKEN_STEP = {
// because saving a broken template opens a recovery page with no assets
@ -10,98 +8,110 @@ var BROKEN_STEP = {
// to properly wait for the page to be saved & reloaded in order to fix the
// race condition of a tour ending on a side-effect (with the possible
// exception of somehow telling the harness / browser to do it)
trigger: 'body',
run: function () {}
trigger: "body",
};
wTourUtils.registerWebsitePreviewTour('test_reset_page_view_complete_flow_part1', {
test: true,
url: '/test_page_view',
// 1. Edit the page through Edit Mode, it will COW the view
edition: true,
},
[
registerWebsitePreviewTour(
"test_reset_page_view_complete_flow_part1",
{
url: "/test_page_view",
// 1. Edit the page through Edit Mode, it will COW the view
edition: true,
},
() => [
{
content: "drop a snippet",
trigger: ".oe_snippet:has(.s_cover) .oe_snippet_thumbnail",
content: "Drag the Intro snippet group and drop it in #oe_structure_test_website_page.",
trigger:
".o_block_tab:not(.o_we_ongoing_insertion) #snippet_groups .o_snippet[name='Intro'] .o_snippet_thumbnail .o_snippet_thumbnail_area",
// id starting by 'oe_structure..' will actually create an inherited view
run: "drag_and_drop iframe #oe_structure_test_website_page",
run: "drag_and_drop :iframe #oe_structure_test_website_page",
},
{
content: "save the page",
extra_trigger: 'iframe #oe_structure_test_website_page.o_dirty',
trigger: "button[data-action=save]",
content: "Click on the s_cover snippet.",
trigger: ':iframe .o_snippet_preview_wrap[data-snippet-id="s_cover"]',
run: "click",
},
...clickOnSave(),
// 2. Edit that COW'd view in the HTML editor to break it.
{
content: "open site menu",
extra_trigger: "iframe body:not(.editor_enable)",
trigger: 'button[data-menu-xmlid="website.menu_site"]',
run: "click",
},
{
content: "open html editor",
trigger: 'a[data-menu-xmlid="website.menu_ace_editor"]',
run: "click",
},
{
content: "add a broken t-field in page DOM",
trigger: 'div.ace_line .ace_xml:contains("placeholder")',
run: function () {
ace.edit('ace-view-editor').getSession().insert({row: 4, column: 1}, '<t t-field="not.exist"/>\n');
run() {
ace.edit(document.querySelector("#resource-editor div"))
.getSession()
.insert({row: 4, column: 1}, '<t t-field="not.exist"/>\n');
},
},
{
content: "save the html editor",
extra_trigger: '.ace_content:contains("not.exist")',
trigger: ".o_ace_view_editor button[data-action=save]",
trigger: '.ace_content:contains("not.exist")',
},
BROKEN_STEP
{
content: "save the html editor",
trigger: ".o_resource_editor button:contains(Save)",
run: "click",
},
BROKEN_STEP,
]
);
wTourUtils.registerWebsitePreviewTour('test_reset_page_view_complete_flow_part2', {
test: true,
url: '/test_page_view',
},
[
registerWebsitePreviewTour(
"test_reset_page_view_complete_flow_part2",
{
url: "/test_page_view",
},
() => [
{
content: "check that the view got fixed",
trigger: 'iframe p:containsExact("Test Page View")',
run: function () {}, // it's a check
trigger: ":iframe p:text(Test Page View)",
},
{
content: "check that the inherited COW view is still there (created during edit mode)",
trigger: 'iframe #oe_structure_test_website_page .s_cover',
run: function () {}, // it's a check
trigger: ":iframe #oe_structure_test_website_page .s_cover",
},
//4. Now break the inherited view created when dropping a snippet
{
content: "open site menu",
trigger: 'button[data-menu-xmlid="website.menu_site"]',
run: "click",
},
{
content: "open html editor",
trigger: 'a[data-menu-xmlid="website.menu_ace_editor"]',
run: "click",
},
{
content: "select oe_structure view",
trigger: '#s2id_ace-view-list', // use select2 version
run: function () {
var viewId = $('#ace-view-list option:contains("oe_structure_test_website_page")').val();
$('#ace-view-list').val(viewId).trigger('change');
},
trigger: ".o_resource_editor_title .o_select_menu_toggler",
run: "click",
},
{
content: "select oe_structure view",
trigger: ".o_select_menu_menu .o_select_menu_item:contains(Test Page View)",
run: "click",
},
{
content: "add a broken t-field in page DOM",
trigger: 'div.ace_line .ace_xml:contains("oe_structure_test_website_page")',
run: function () {
ace.edit('ace-view-editor').getSession().insert({row: 4, column: 1}, '<t t-field="not.exist"/>\n');
run() {
ace.edit(document.querySelector("#resource-editor div"))
.getSession()
.insert({row: 4, column: 1}, '<t t-field="not.exist"/>\n');
},
},
{
content: "save the html editor",
trigger: ".o_ace_view_editor button[data-action=save]",
trigger: ".o_resource_editor button:contains(Save)",
run: "click",
},
BROKEN_STEP
BROKEN_STEP,
]
);
});

View file

@ -0,0 +1,174 @@
import {
clickOnSave,
clickOnEditAndWaitEditMode,
clickOnExtraMenuItem,
registerWebsitePreviewTour,
insertSnippet
} from '@website/js/tours/tour_utils';
import { stepUtils } from "@web_tour/tour_utils";
const EDIT_BUTTON_SELECTOR = "body .o_menu_systray button.o-website-btn-custo-primary:contains(edit)";
const checkNoTranslate = {
content: "Check there is no translate button",
trigger: `${EDIT_BUTTON_SELECTOR}:not(.o-dropdown-toggle-custo)`,
};
const translate = [{
content: "Open Edit menu",
trigger: `${EDIT_BUTTON_SELECTOR}.o-dropdown-toggle-custo`,
run: "click",
}, {
content: "Click on translate button",
trigger: ".o_popover .o_translate_website_dropdown_item:contains(translate)",
run: "click",
}];
const closeErrorDialog = [{
content: "Check has error dialog",
trigger: ".modal:contains(error) .o_error_dialog.modal-content",
}, {
content: "Close error dialog",
trigger: ".modal .modal-footer button.btn.btn-primary",
run: "click",
}, {
trigger: "body:not(:has(.modal))",
}];
const switchTo = (lang) => [
{
content: `Switch to ${lang}`,
trigger: `:iframe .js_change_lang[data-url_code='${lang}']`,
run: "click",
},
{
content: `Wait until ${lang} is applied`,
trigger: `:iframe html[lang*="${lang}"]`,
},
stepUtils.waitIframeIsReady(),
];
const goToMenuItem = [
clickOnExtraMenuItem({}, true),
{
content: "Navigate to model item page",
trigger: ":iframe a[href='/test_website/model_item/1']",
run: "click",
},
{
content: "Wait to land on model item page",
trigger: ':iframe a[href="/test_website/model_item/1"].nav-link.active:not(:visible)',
},
stepUtils.waitIframeIsReady(),
];
registerWebsitePreviewTour('test_restricted_editor_only', {
url: '/',
}, () => [
// Home
checkNoTranslate,
...clickOnEditAndWaitEditMode(),
{
content: "Check icons cannot be dragged",
trigger: "#snippet_groups .o_snippet[name='Intro'].o_disabled",
run: function () {
if (document.querySelector("button.o_snippet_thumbnail_area")) {
console.error(
"The button to open the add snippet dialog should not be display for restricted editor."
);
}
},
},
...clickOnSave(),
...switchTo('fr'),
...translate,
...closeErrorDialog,
...switchTo('en'),
// Model item
{
trigger: ":iframe body:contains(welcome to your)"
},
...goToMenuItem,
checkNoTranslate,
...clickOnEditAndWaitEditMode(),
{
content: "Check icons cannot be dragged",
trigger: "#snippet_groups .o_snippet[name='Intro'].o_disabled",
run: function () {
if (document.querySelector("button.o_snippet_thumbnail_area")) {
console.error(
"The button to open the add snippet dialog should not be display for restricted editor."
);
}
},
},
...clickOnSave(),
...switchTo('fr'),
...translate,
...closeErrorDialog,
]);
registerWebsitePreviewTour('test_restricted_editor_test_admin', {
url: '/',
}, () => [
// Home
checkNoTranslate,
...clickOnEditAndWaitEditMode(),
{
content: "Check icons cannot be dragged",
trigger: "#snippet_groups .o_snippet[name='Intro'].o_disabled",
},
...clickOnSave(),
...switchTo('fr'),
...translate,
...closeErrorDialog,
...switchTo('en'),
// Model item
...goToMenuItem,
checkNoTranslate,
...clickOnEditAndWaitEditMode(),
{
content: "Check icons can be dragged",
trigger: "#snippet_groups .o_snippet[name='Intro']:not(.o_disabled)",
},
...insertSnippet({ id: "s_banner", name: "Banner", groupName: "Intro" }),
{
content: "Change name",
trigger: ":iframe [data-oe-expression='record.name']",
run: "editor New value",
},
...clickOnSave(),
...switchTo('fr'),
...translate,
{
content: "Close the dialog",
trigger: ".modal .modal-footer .btn-primary",
run: "click",
},
{
content: "Assure the modal is well closed",
trigger: "body:not(:has(.modal))",
},
{
content: "Check that html fields are not content editable when translating",
trigger: ":iframe [data-oe-expression='record.website_description']:not([contenteditable='true'])",
},
{
content: "Translate name",
trigger: ":iframe [data-oe-expression='record.name']",
run: "editor Nouvelle valeur",
},
{
content: "Translate some banner text",
trigger: ":iframe [data-oe-expression='record.website_description'] strong",
run: "editor potentiel.",
},
...clickOnSave(),
]);
registerWebsitePreviewTour('test_restricted_editor_tester', {
url: '/test_model/1',
}, () => [
...clickOnEditAndWaitEditMode(),
{
content: "Footer should not be be editable for restricted user",
trigger: ":iframe :has(.o_editable) footer:not(.o_editable):not(:has(.o_editable))",
},
...clickOnSave(),
]);

View file

@ -0,0 +1,83 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { VideoSelector } from "@html_editor/main/media/media_dialog/video_selector";
import { insertSnippet, registerWebsitePreviewTour } from "@website/js/tours/tour_utils";
registerWebsitePreviewTour(
"snippet_background_video",
{
url: "/",
edition: true,
}, () => [
{
trigger: "body",
run: function () {
// Patch the VideoDialog so that it does not do external calls
// during the test (note that we don't unpatch but as the patch
// is only done after the execution of a test_website test, it
// is acceptable).
// TODO we should investigate to rather mock the external calls,
// maybe not using a tour. Probably easier to discuss when the
// new OWL editor will have been implemented.
patch(VideoSelector.prototype, {
async prepareVimeoPreviews() {
// Ignore the super call and directly push a fake video
this.state.vimeoPreviews.push({
id: 1,
// Those lead to 404 but it's fine for the test
thumbnailSrc: "/hello/world.jpg",
src: "/hello/world.mp4",
});
},
async _getVideoURLData(src, options) {
if (src === '/hello/world.mp4') {
return {
'platform': 'vimeo',
'embed_url': 'about:blank',
};
}
return super._getVideoURLData(...arguments);
},
});
},
},
...insertSnippet({
id: "s_text_block",
name: "Text",
groupName: "Text",
}),
{
content: "Click on the text block.",
trigger: ":iframe #wrap section.s_text_block",
run: "click",
},
{
content: "Click on the 'Background Video' button option.",
trigger: "button[data-action-id='toggleBgVideo']",
run: "click",
},
{
content: "Click on the first sample video in the modal.",
trigger: "#video-suggestion .o_sample_video",
run: "click",
},
{
content: "Check the video is select.",
trigger: "textarea.is-valid",
},
{
content: "Click on the 'Add' button to apply the selected video as the background.",
trigger: ".modal-footer button.btn-primary",
run: "click",
},
{
content: "Verify that the video is set as the background of the snippet.",
trigger: ":iframe #wrap section.o_background_video",
},
{
content: "Check that the video container is not editable.",
trigger: ":iframe #wrap section.o_background_video > .o_bg_video_container[contenteditable=false]",
},
]
);

View file

@ -1,6 +1,10 @@
/** @odoo-module **/
import wTourUtils from 'website.tour_utils';
import {
clickOnEditAndWaitEditMode,
clickOnSave,
registerWebsitePreviewTour,
} from '@website/js/tours/tour_utils';
import { stepUtils } from "@web_tour/tour_utils";
/**
* The purpose of these tours is to check the systray visibility:
@ -12,189 +16,209 @@ import wTourUtils from 'website.tour_utils';
* - as an unrelated user (neither "tester" nor restricted editor)
*/
const canPublish = [{
content: 'Publish',
const canPublish = () => [{
content: "Publish",
trigger: '.o_menu_systray .o_menu_systray_item:contains("Unpublished")',
run: "click",
}, {
content: 'Wait for Publish',
content: "Wait for Publish",
trigger: '.o_menu_systray .o_menu_systray_item:contains("Published"):not([data-processing])',
run: () => {}, // This is a check.
}, {
content: 'Unpublish',
content: "Unpublish",
trigger: '.o_menu_systray .o_menu_systray_item:contains("Published")',
run: "click",
}, {
content: 'Wait for Unpublish',
content: "Wait for Unpublish",
trigger: '.o_menu_systray .o_menu_systray_item:contains("Unpublished"):not([data-processing])',
run: () => {}, // This is a check.
}];
const cannotPublish = [{
content: 'Check has no Publish/Unpublish',
const cannotPublish = () => [{
content: "Check has no Publish/Unpublish",
trigger: '.o_menu_systray:not(:has(.o_menu_systray_item:contains("ublished")))',
run: () => {}, // This is a check.
}];
const canToggleMobilePreview = [{
content: 'Enable mobile preview',
trigger: '.o_menu_systray .o_menu_systray_item.o_mobile_preview:not(.o_mobile_preview_active)',
const canToggleMobilePreview = () => [{
content: "Enable mobile preview",
trigger: '.o_menu_systray .o_menu_systray_item.o_mobile_preview:not(.o_mobile_preview_active) span',
run: "click",
}, {
content: 'Disable mobile preview',
trigger: '.o_menu_systray .o_menu_systray_item.o_mobile_preview.o_mobile_preview_active',
content: "Disable mobile preview",
trigger: '.o_menu_systray .o_menu_systray_item.o_mobile_preview.o_mobile_preview_active span',
run: "click",
}];
const cannotToggleMobilePreview = [{
const cannotToggleMobilePreview = () => [{
content: 'Enable mobile preview',
trigger: '.o_menu_systray:not(:has(.o_menu_systray_item.o_mobile_preview))',
run: () => {}, // This is a check.
}];
// For non-website users, switching across website only works if the domains are
// specified. Within the scope of test tours, this cannot be achieved.
const canSwitchWebsiteNoCheck = [{
const canSwitchWebsiteNoCheck = () => [{
content: 'Open website switcher',
trigger: '.o_menu_systray .o_menu_systray_item.o_website_switcher_container .dropdown-toggle:contains("My Website"):not(:contains("My Website 2"))',
run: "click",
}, {
content: 'Switch to other website',
trigger: '.o_menu_systray .o_menu_systray_item.o_website_switcher_container .dropdown-item:contains("Other")',
run: () => {}, // This is a check.
content: 'Can switch to other website',
trigger: '.o-dropdown--menu .dropdown-item:contains("Other")',
}];
const canSwitchWebsite = [{
content: 'Open website switcher',
const canSwitchWebsite = () => [{
content: "Open website switcher",
trigger: '.o_menu_systray .o_menu_systray_item.o_website_switcher_container .dropdown-toggle:contains("My Website"):not(:contains("My Website 2"))',
run: "click",
}, {
content: 'Switch to other website',
trigger: '.o_menu_systray .o_menu_systray_item.o_website_switcher_container .dropdown-item:contains("Other")',
content: "Switch to other website",
trigger: '.o-dropdown--menu .dropdown-item:contains("Other")',
run: "click",
}, {
content: 'Wait for other website',
trigger: 'iframe body:contains("Test Model") div:contains("Other")',
run: () => {}, // This is a check.
content: "Wait for other website",
trigger: ':iframe body:contains("Test Model") div:contains("Other")',
}];
const canAddNewContent = [{
content: 'Open +New content',
trigger: '.o_menu_systray .o_menu_systray_item.o_new_content_container',
const canAddNewContent = () => [{
content: "Open +New content",
trigger: '.o_menu_systray .o_menu_systray_item.o_new_content_container button',
run: "click",
}, {
content: 'Close +New content',
trigger: '#o_new_content_menu_choices',
content: "Close +New content",
trigger: '.o_new_content_menu_choices',
run: "click",
}];
const cannotAddNewContent = [{
const cannotAddNewContent = () => [{
content: 'No +New content',
trigger: '.o_menu_systray:not(:has(.o_menu_systray_item.o_new_content_container))',
run: () => {}, // This is a check.
}];
const canEditInBackEnd = [{
content: 'Edit in backend',
const canEditInBackEnd = () => [{
content: "Edit in backend",
trigger: '.o_menu_systray .o_website_edit_in_backend a',
run: "click",
}, {
content: 'Check that the form is editable',
content: "Check that the form is editable",
trigger: '.o_form_view_container .o_form_editable',
run: () => {}, // This is a check.
}, {
content: 'Return to website',
trigger: '.oe_button_box .fa-globe',
content: "Return to website",
trigger: '.o-form-buttonbox .fa-globe',
run: "click",
}];
const canViewInBackEnd = [{
content: 'Go to backend',
const canViewInBackEnd = () => [{
content: "Go to backend",
trigger: '.o_menu_systray .o_website_edit_in_backend a',
run: "click",
}, {
content: 'Check that the form is read-only',
content: "Check that the form is read-only",
trigger: '.o_form_view_container .o_form_readonly',
run: () => {}, // This is a check.
}, {
content: 'Return to website',
trigger: '.oe_button_box .fa-globe',
content: "Return to website",
trigger: '.o-form-buttonbox .fa-globe',
run: "click",
}];
const canEdit = [
...wTourUtils.clickOnEditAndWaitEditMode(),
const canEdit = () => [
...clickOnEditAndWaitEditMode(),
{
content: 'Click on name',
trigger: 'iframe span[data-oe-expression="test_model.name"][contenteditable="true"]',
content: "Click on name",
trigger: ':iframe span[data-oe-expression="test_model.name"][contenteditable="true"]',
run: "click",
}, {
content: 'Change name',
trigger: 'iframe span[data-oe-expression="test_model.name"][contenteditable="true"]',
run: 'text Better name',
content: "Change name",
trigger: ':iframe span[data-oe-expression="test_model.name"][contenteditable="true"]',
run: "editor Better name",
}, {
content: 'Check that field becomes dirty',
trigger: 'iframe span[data-oe-expression="test_model.name"].o_dirty',
run: () => {}, // This is a check.
content: "Check that field becomes dirty",
trigger: ':iframe span[data-oe-expression="test_model.name"].o_dirty',
},
...wTourUtils.clickOnSave(),
...clickOnSave(),
{
content: 'Check whether name is saved',
trigger: 'iframe span[data-oe-expression="test_model.name"]:contains("Better name")',
run: () => {}, // This is a check.
content: "Check whether name is saved",
trigger: ':iframe span[data-oe-expression="test_model.name"]:contains("Better name")',
},
];
const cannotEdit = [{
content: 'Check Edit is not available',
const cannotEdit = () => [stepUtils.waitIframeIsReady(), {
content: "Check Edit is not available",
trigger: '.o_menu_systray:not(:has(.o_edit_website_container))',
run: () => {}, // This is a check.
}];
const canEditButCannotChange = [
...wTourUtils.clickOnEditAndWaitEditMode(),
const canEditButCannotChange = () => [
...clickOnEditAndWaitEditMode(),
{
content: 'Cannot change name',
trigger: 'iframe main:not(:has([data-oe-expression])):contains("Test Model")',
run: () => {}, // This is a check.
trigger: ':iframe main:not(:has([data-oe-expression])):contains("Test Model")',
},
];
const ensureWebsiteSwitcherIsNotVisible = [
{
content: "Ensure website switcher is hidden when only one website exists",
trigger: ".o_menu_systray:not(:has(.o_website_switcher_container))",
},
];
const ensureWebsiteSwitcherIsVisible = [
{
content: "Ensure website switcher is present when multiple website exists",
trigger: ".o_menu_systray:has(.o_website_switcher_container)",
},
];
const register = (title, steps) => {
wTourUtils.registerWebsitePreviewTour(title, {
url: '/test_model/1',
test: true,
registerWebsitePreviewTour(title, {
url: "/test_model/1",
}, steps);
};
register('test_systray_admin', [
...canPublish,
...canToggleMobilePreview,
...canSwitchWebsite,
...canAddNewContent,
...canEditInBackEnd,
...canEdit,
register("test_systray_admin", () => [
...canPublish(),
...canToggleMobilePreview(),
...canSwitchWebsite(),
...canAddNewContent(),
...canEditInBackEnd(),
...canEdit(),
]);
register('test_systray_reditor_tester', [
...canPublish,
...canToggleMobilePreview,
...canSwitchWebsite,
...canAddNewContent,
...canEditInBackEnd,
...canEdit,
register("test_systray_reditor_tester", () => [
...canPublish(),
...canToggleMobilePreview(),
...canSwitchWebsite(),
...canAddNewContent(),
...canEditInBackEnd(),
...canEdit(),
]);
register('test_systray_reditor_not_tester', [
...cannotPublish,
...canToggleMobilePreview,
...canSwitchWebsite,
...canAddNewContent,
...canViewInBackEnd,
...canEditButCannotChange,
register("test_systray_reditor_not_tester", () => [
...cannotPublish(),
...canToggleMobilePreview(),
...canSwitchWebsite(),
...canAddNewContent(),
...canViewInBackEnd(),
...canEditButCannotChange(),
]);
register('test_systray_not_reditor_tester', [
...canPublish,
...cannotToggleMobilePreview,
...canSwitchWebsiteNoCheck,
...cannotAddNewContent,
...canEditInBackEnd,
...cannotEdit,
register("test_systray_not_reditor_tester", () => [
...canPublish(),
...cannotToggleMobilePreview(),
...canSwitchWebsiteNoCheck(),
...cannotAddNewContent(),
...canEditInBackEnd(),
...cannotEdit(),
]);
register('test_systray_not_reditor_not_tester', [
...cannotPublish,
...cannotToggleMobilePreview,
...canSwitchWebsiteNoCheck,
...cannotAddNewContent,
...canViewInBackEnd,
...cannotEdit,
register("test_systray_not_reditor_not_tester", () => [
...cannotPublish(),
...cannotToggleMobilePreview(),
...canSwitchWebsiteNoCheck(),
...cannotAddNewContent(),
...canViewInBackEnd(),
...cannotEdit(),
{
trigger: ":iframe main:contains(test model)",
},
]);
register("test_systray_single_website", () => ensureWebsiteSwitcherIsNotVisible);
register("test_systray_multi_website", () => ensureWebsiteSwitcherIsVisible);

View file

@ -0,0 +1,499 @@
import {
clickOnEditAndWaitEditMode,
clickOnSave,
insertSnippet,
registerWebsitePreviewTour,
} from "@website/js/tours/tour_utils";
import { stepUtils } from "@web_tour/tour_utils";
import { translationIsReady } from "@web/core/l10n/translation";
function createNewPage() {
return [
{
content: "Open +New content",
trigger: ".o_menu_systray .o_menu_systray_item.o_new_content_container button",
run: "click",
},
{
content: "Create a New page",
trigger: `button.o_new_content_element img[src="/website/static/description/icon.png"]`,
run: "click",
},
{
content: "Select Blank page",
trigger: ".o_page_template:has(div.text-muted) .o_button_area:not(:visible)",
run: "click",
},
{
content: "Page name",
trigger: ".modal-dialog .o_website_dialog input",
run: "edit Test",
},
{
content: "Confirm creation",
trigger: ".modal-dialog .o_website_dialog .btn-primary",
run: "click",
},
{
trigger: ".o_builder_sidebar_open",
timeout: 20000,
},
...insertSnippet({
id: "s_banner",
name: "Banner",
groupName: "Intro",
}),
{
content: "Click on the link",
trigger: ":iframe main section.s_banner a",
async run(helpers) {
await helpers.click();
const el = this.anchor;
const sel = el.ownerDocument.getSelection();
sel.collapse(el, 0);
el.focus();
},
},
{
content: "Click on Edit link",
trigger: ".o_we_edit_link",
run: "click",
},
{
content: "Replace URL",
trigger: ".o-we-linkpopover .o_we_href_input_link",
run: "edit /test_view",
},
{
content: "Apply",
trigger: ".o-we-linkpopover .btn-primary",
run: "click",
},
...clickOnSave("bottom", 50000, false),
];
}
function openHtmlEditor() {
return [
{
content: "Open Site menu",
trigger: ".o_menu_sections [data-menu-xmlid='website.menu_site']",
run: "click",
},
{
content: "Open HTML editor",
trigger: ".o-overlay-item .dropdown-item[data-menu-xmlid='website.menu_ace_editor']",
run: "click",
},
{
content: "Edit anyway",
trigger: ".o_resource_editor_wrapper [role='alert'] button.btn-link",
run: "click",
},
];
}
function saveHtmlEditor() {
return [
{
content: "Save the html editor",
trigger: ".o_resource_editor button.btn-primary",
run: "click",
},
{
content: "Close the html editor",
trigger: ".o_resource_editor button.btn-secondary",
run: "click",
},
];
}
function singleLanguage() {
return [
{
content: "Ensure single language site",
trigger: ":iframe body:not(:has(.js_language_selector))",
},
// new page
...createNewPage(),
...openHtmlEditor(),
{
content: "Change text",
trigger: 'div.ace_line .ace_xml:contains("oe_structure")',
run() {
window.ace.edit(document.querySelector("#resource-editor div"))
.getSession()
.insert({row: 8, column: 1}, '<p>More text</p>\n');
},
},
...saveHtmlEditor(),
{
content: "Ensure page is updated",
trigger: ":iframe body:contains(More text)",
},
// xml record
{
content: "Go to test_view page",
trigger: ":iframe main section.s_banner a",
run: "click",
},
{
content: "Wait until page is reached",
trigger: ":iframe body:contains(Test View)",
},
...clickOnEditAndWaitEditMode(),
{
content: "Edit template text",
trigger: ":iframe main p.o_editable[data-oe-field='arch'][contenteditable='true']",
run: "editor Modified Text",
},
...clickOnSave("bottom", 50000, false),
...openHtmlEditor(),
{
content: "Change text",
trigger: 'div.ace_line .ace_xml:contains("test_website.test_view")',
run() {
window.ace.edit(document.querySelector("#resource-editor div"))
.getSession()
.insert({row: 2, column: 36}, 'Further ');
},
},
...saveHtmlEditor(),
{
content: "Ensure view is updated",
trigger: ":iframe body:contains(Further Modified Text)",
},
];
}
const ensureFrUser = {
content: "Ensure FR user",
trigger: ".o_website_systray:contains(Publié)",
};
const ensureEnUser = {
content: "Ensure EN user",
trigger: ".o_website_systray:contains(Published)",
};
const ensureFrSite = {
content: "Ensure FR site",
trigger: ":iframe .o_main_nav:contains(Accueil)",
};
const ensureEnSite = {
content: "Ensure EN site",
trigger: ":iframe .o_main_nav:contains(Home)",
};
registerWebsitePreviewTour(
"translation_single_language_fr_user_fr_site",
{
url: "/",
},
() => [
ensureFrUser,
ensureFrSite,
...singleLanguage(),
]
);
registerWebsitePreviewTour(
"translation_single_language_en_user_fr_site",
{
url: "/",
},
() => [
ensureEnUser,
ensureFrSite,
...singleLanguage(),
]
);
registerWebsitePreviewTour(
"translation_single_language_fr_user_en_site",
{
url: "/",
},
() => [
ensureFrUser,
ensureEnSite,
...singleLanguage(),
]
);
function switchLanguage(lang, timeout = 50000) {
return [
{
content: "Ensure was in other language",
trigger: `:iframe .o_header_language_selector:contains(${lang !== "fr" ? "Français" : "English"})`,
timeout,
}, {
content: "Open language dropdown",
trigger: ":iframe .o_header_language_selector .dropdown-toggle",
run: "click",
}, {
content: "Select language",
trigger: `:iframe .o_header_language_selector .js_change_lang[data-url_code=${lang}]`,
run: "click",
}, {
content: "Wait until target page is loaded",
trigger: `:iframe .o_header_language_selector:contains(${lang === "fr" ? "Français" : "English"})`,
timeout,
}
];
}
// TODO Such a step should not be needed, but test randomly fails without it.
const awaitTranslationIsReady = {
content: "Await translationIsReady",
trigger: "body",
run: async () => {
await translationIsReady;
},
};
function openTranslate(timeout = 50000) {
return [
stepUtils.waitIframeIsReady(),
awaitTranslationIsReady,
{
content: "Open edit dropdown",
trigger: ".o_edit_website_container button",
run: "click",
}, {
content: "Enter translate mode",
trigger: ".o_translate_website_dropdown_item",
run: "click",
}, {
content: "Effect's 200ms setTimeout passed",
trigger: ".o_builder_open .o_main_navbar.d-none:not(:visible)",
}, {
content: "Translatable text became highlighted",
trigger: ":iframe [data-oe-translation-state=to_translate]",
timeout,
}, {
content: "Confirm popup",
trigger: ".o_website_dialog .btn-secondary",
run: "click",
}
];
}
function saveTranslation(timeout = 50000) {
return [
{
content: "Save translation",
trigger: ".o-website-builder_sidebar button[data-action=save]",
run: "click",
}, {
content: "Back to preview mode",
trigger: ".o_edit_website_container button",
timeout,
}, {
trigger: "body:not(.o_builder_open)",
noPrepend: true,
timeout,
},
stepUtils.waitIframeIsReady(),
awaitTranslationIsReady,
];
}
function multiLanguage(mainLanguage, secondLanguage) {
return [
{
content: "Ensure multi language site",
trigger: ":iframe body:has(.js_language_selector)",
},
// new page
...createNewPage(),
...switchLanguage(secondLanguage),
...openTranslate(),
{
content: "Translate some text",
trigger: ":iframe h1 [data-oe-translation-state=to_translate]",
run: "editor Some translated text",
},
...saveTranslation(),
{
content: "Check translation is displayed",
trigger: ":iframe h1:contains(Some translated text)",
},
...openHtmlEditor(),
{
content: "Change text",
trigger: 'div.ace_line .ace_xml:contains("oe_structure")',
run() {
window.ace.edit(document.querySelector("#resource-editor div"))
.getSession()
.insert({row: 6, column: 50}, "more text ");
},
},
{
content: "Pollute old DOM to detect reload",
trigger: ":iframe body",
run() {
this.anchor.dataset.reloaded = false;
},
},
...saveHtmlEditor(),
{
content: "Ensure page is NOT updated",
trigger: ":iframe body:not([data-reloaded=false]) h1:not(:contains(more text))",
},
...switchLanguage(mainLanguage),
{
content: "Ensure French page IS updated",
trigger: ":iframe h1:contains(more text)",
},
...clickOnEditAndWaitEditMode(),
{
content: "Change text again",
trigger: ":iframe h1",
run: "editor Yet another version of the text.",
},
...clickOnSave("bottom", 50000, false),
...switchLanguage(secondLanguage),
{
content: "Ensure English page is NOT updated",
trigger: ":iframe h1:not(:contains(Yet another))",
},
...openTranslate(),
{
content: "Ensure English page is updated",
trigger: ":iframe h1:contains(Yet another)",
},
{
content: "Translate again",
trigger: ":iframe h1 [data-oe-translation-state=to_translate]",
run: "editor Yet another translated text",
},
...saveTranslation(),
{
content: "Check translation is displayed",
trigger: ":iframe h1:contains(Yet another translated text)",
},
// xml record
{
content: "Go to test_view page",
trigger: ":iframe main section.s_banner a",
run: "click",
},
{
content: "Wait until page is reached",
trigger: ":iframe body:contains(Test View)",
},
...switchLanguage(mainLanguage),
...clickOnEditAndWaitEditMode(),
{
content: "Edit template text",
trigger: ":iframe main p.o_editable[contenteditable='true']",
run: "editor Modified View",
},
...clickOnSave("bottom", 50000, false),
...switchLanguage(secondLanguage),
...openTranslate(),
{
content: "Translate test view",
trigger: ":iframe main > p > span[data-oe-translation-state=to_translate]",
run: "editor Some translated view",
},
...saveTranslation(),
{
content: "Check translation is displayed",
trigger: ":iframe p:contains(Some translated view)",
},
...switchLanguage(mainLanguage),
...openHtmlEditor(),
{
content: "Change text",
trigger: 'div.ace_line .ace_xml:contains("test_website.test_view")',
run() {
window.ace.edit(document.querySelector("#resource-editor div"))
.getSession()
.insert({row: 2, column: 36}, 'Further ');
},
},
...saveHtmlEditor(),
{
content: "Ensure view is updated",
trigger: ":iframe body:contains(Further Modified View)",
},
...clickOnEditAndWaitEditMode(),
{
content: "Edit template text",
trigger: ":iframe main p.o_editable[data-oe-field='arch'][contenteditable='true']",
run: "editor Even more modified Text",
},
...clickOnSave("bottom", 50000, false),
...switchLanguage(secondLanguage),
{
content: "Check old translation is displayed",
trigger: ":iframe p:contains(Some translated view)",
},
...openTranslate(),
{
content: "Check new original is displayed",
trigger: ":iframe p:contains(Even more modified text)",
},
{
content: "Translate test view",
trigger: ":iframe main > p > span[data-oe-translation-state=to_translate]",
run: "editor Even more translated text",
},
...saveTranslation(),
{
content: "Check new translation is displayed",
trigger: ":iframe p:contains(Even more translated text)",
},
];
}
registerWebsitePreviewTour(
"translation_multi_language_fr_user_fr_en_site",
{
url: "/fr",
},
() => [
ensureFrUser,
ensureFrSite,
...multiLanguage("fr", "en"),
]
);
registerWebsitePreviewTour(
"translation_multi_language_fr_user_en_fr_site",
{
url: "/en",
},
() => [
ensureFrUser,
ensureEnSite,
...multiLanguage("en", "fr"),
]
);
registerWebsitePreviewTour(
"translation_multi_language_en_user_fr_en_site",
{
url: "/fr",
},
() => [
ensureEnUser,
ensureFrSite,
...multiLanguage("fr", "en"),
]
);
registerWebsitePreviewTour(
"translation_multi_language_en_user_en_fr_site",
{
url: "/en",
},
() => [
ensureEnUser,
ensureEnSite,
...multiLanguage("en", "fr"),
]
);

View file

@ -0,0 +1,66 @@
import {
clickOnSave,
registerWebsitePreviewTour,
changeOptionInPopover,
} from "@website/js/tours/tour_utils";
function assertEqual(actual, expected) {
if (actual !== expected) {
throw new Error(`Assert failed: expected: ${expected} ; got: ${actual}`);
}
}
registerWebsitePreviewTour('website_controller_page_listing_layout', {
url: '/model/exposed-model',
edition: true,
}, () => [
{
content: "website is in preview mode",
trigger: '.o_website_preview',
run: "click",
},
{
content: "records are listed in grid mode by default",
trigger: ':iframe .o_website_grid',
run() {
const iframeDocument = document.querySelector(".o_website_preview iframe").contentDocument;
// grid option is selected by default in the switch
assertEqual(iframeDocument.querySelector(".listing_layout_switcher #o_wstudio_apply_grid").checked, true);
assertEqual([...iframeDocument.querySelectorAll(".test_record_listing")].length, 2);
},
},
{
content: "open customize tab",
trigger: ":iframe .listing_layout_switcher",
run: "click",
},
{
trigger: ".o-snippets-menu .o_customize_tab",
},
...changeOptionInPopover("Layout", "Default Layout", "list"),
{
content: "records are now displayed in list mode",
trigger: ':iframe .o_website_list',
run() {
const iframeDocument = document.querySelector(".o_website_preview iframe").contentDocument;
// list option is now selected in the switch
assertEqual(iframeDocument.querySelector(".listing_layout_switcher #o_wstudio_apply_list").checked, true);
},
},
...clickOnSave(),
]);
registerWebsitePreviewTour('website_controller_page_default_page_check', {
url: '/model/exposed-model',
}, () => [
{
content: "records are listed in list mode by default",
trigger: ':iframe [is-ready=true] .o_website_list',
run() {
const iframeDocument = document.querySelector(".o_website_preview iframe").contentDocument;
// list option is selected by default in the switch
assertEqual(iframeDocument.querySelector(".listing_layout_switcher #o_wstudio_apply_list").checked, true);
assertEqual([...iframeDocument.querySelectorAll(".test_record_listing")].length, 2);
},
},
]);

View file

@ -0,0 +1,60 @@
import { clickOnSave, registerWebsitePreviewTour } from "@website/js/tours/tour_utils";
// As admin, add a YouTube video iframe in a `sanitize_overridable` HTML field.
registerWebsitePreviewTour("website_designer_iframe_video",
{
url: "/test_website/model_item/1",
edition: true,
},
() => [
{
content: "As administrator, add a video block to the description field",
trigger: `.o_block_tab:not(.o_we_ongoing_insertion) #snippet_content .o_snippet[name="Video"].o_draggable .o_snippet_thumbnail`,
run: "drag_and_drop :iframe .o_test_website_description",
},
{
content: "Add a video URL",
trigger: ".o_select_media_dialog #o_video_text",
run: `edit https://www.youtube.com/watch?v=G8b4UZIcTfg`,
},
{
content: "Add the video",
trigger: ".o_select_media_dialog .modal-footer .btn-primary",
run: "click",
},
...clickOnSave(),
{
content: "Check that the video was correctly saved",
trigger: ":iframe .media_iframe_video[data-oe-expression*='G8b4UZIcTfg']",
run: () => {},
},
]
);
// Check that a restricted editor can edit the field content (even with
// a video iframe).
registerWebsitePreviewTour("website_restricted_editor_iframe_video", {
url: "/test_website/model_item/1",
edition: true,
},
() => [
{
content: "Check that the video iframe was correctly restored after saving the changes",
trigger:
":iframe [data-oe-field]:not([data-oe-sanitize-prevent-edition]) .media_iframe_video[data-oe-expression*='G8b4UZIcTfg']",
run: () => {},
},
{
content: "As a restricted editor, edit the HTML field content",
trigger: ":iframe .o_test_website_description",
run: "editor I can still edit the HTML field",
},
...clickOnSave(),
{
content: "Check that the HTML content (with a video iframe) was correctly updated",
trigger:
":iframe .o_test_website_description:contains('I can still edit the HTML field')",
run: () => {},
},
]
);

View file

@ -0,0 +1,382 @@
import {
assertPathName,
clickOnSave,
getClientActionUrl,
registerWebsitePreviewTour,
} from "@website/js/tours/tour_utils";
import { stepUtils } from "@web_tour/tour_utils";
const openPagePropertiesDialog = [
{
content: "Open Site backend menu",
trigger: '[data-menu-xmlid="website.menu_site"]',
run: "click",
},
{
content: "Open page properties dialog",
trigger: '[data-menu-xmlid="website.menu_page_properties"]',
run: "click",
},
];
const clickOnSaveButtonStep = [
{
content: "Click on Save & Close",
trigger: ".o_form_button_save:enabled",
run: "click",
},
{
content: "Wait",
trigger: "body:not(.modal-open)",
}
];
const openCreatePageDialog = [
{
content: "Open create content menu",
trigger: ".o_new_content_container button",
run: "click",
},
{
content: "Create a new page",
trigger: 'button[aria-label="New Page"]',
run: "click",
},
];
function assertPageCanonicalUrlIs(url) {
return [
{
content: `Verify page canonical url is ${url}`,
trigger: `:iframe head:hidden link[rel="canonical"][href$="${url}"]`,
},
];
}
function checkIsTemplate(isTemplate, pageTitle = undefined) {
return [
...openCreatePageDialog,
{
trigger: 'button[data-id="custom"]',
},
{
content: "Go to custom section",
trigger: 'button[data-id="custom"]',
run: "click",
},
...(isTemplate
? [
{
content: `Verify template ${pageTitle} exists`,
trigger: `.o_page_template .o_page_name:contains(${pageTitle}):hidden`,
},
]
: [
{
trigger: ".o_website_page_templates_pane .alert-info",
},
{
content: `Verify custom templates section is empty`,
trigger: `.o_website_page_templates_pane:not(:has(.o_page_template))`,
},
]
),
{
content: "Exit dialog",
trigger: ".modal-header .btn-close",
run: "click",
},
{
content: "Exit new content backdrop",
trigger: "body",
run: "press escape",
}
];
}
function testCommonProperties(url, canPublish, modifiedUrl = undefined) {
if (!modifiedUrl) {
modifiedUrl = url;
}
const steps = {
setup: [
{
content: "Open Edit Menu dialog",
trigger: '.o_field_widget[name="is_in_menu"] + .btn-link',
run: "click",
},
{
content: "Check that menu editor was opened",
trigger: ".oe_menu_editor",
},
{
content: "Close Edit Menu dialog",
trigger: ".modal:has(.oe_menu_editor) .btn-close",
run: "click",
},
{
content: "Add to menu",
trigger: "#is_in_menu_0",
run: "check",
},
{
content: "Set as homepage",
trigger: "#is_homepage_0",
run: "check",
},
],
check: [
{
content: "Verify is in menu",
trigger: `:visible :iframe #top_menu a[href="${modifiedUrl}"]`,
},
stepUtils.goToUrl(getClientActionUrl("/")),
...assertPageCanonicalUrlIs(modifiedUrl),
stepUtils.goToUrl(getClientActionUrl(modifiedUrl)),
],
teardown: [
{
content: "Remove from menu",
trigger: "#is_in_menu_0",
run: "uncheck",
},
{
content: "Unset as homepage",
trigger: "#is_homepage_0",
run: "uncheck",
},
],
checkTorndown: [
{
content: "Verify is not in menu",
trigger: `:visible :iframe #top_menu:not(:has(a[href="${url}"]))`,
},
stepUtils.goToUrl(getClientActionUrl("/")),
...assertPageCanonicalUrlIs("/"),
stepUtils.goToUrl(getClientActionUrl(url)),
],
finalize() {
return [
...openPagePropertiesDialog,
...this.setup,
...clickOnSaveButtonStep,
...this.check,
...openPagePropertiesDialog,
...this.teardown,
...clickOnSaveButtonStep,
...this.checkTorndown,
];
},
};
if (canPublish) {
steps.setup.push({
content: "Publish",
trigger: "#is_published_0",
run: "check",
});
steps.check.push({
content: "Verify is published",
trigger: '[data-hotkey="p"] .form-check input:checked',
});
steps.teardown.push({
content: "Unpublish",
trigger: "#is_published_0",
run: "uncheck",
});
steps.checkTorndown.push({
content: "Verify is not published",
trigger: '[data-hotkey="p"] .form-check input:not(:checked)',
});
}
return steps;
}
function testWebsitePageProperties() {
const steps = testCommonProperties("/new-page", true, "/cool-page");
steps.setup.unshift(
{
content: "Change page title",
trigger: "#name_0",
run: "edit Cool Page",
},
{
content: `Change url to /cool-page`,
trigger: "#url_0",
run: `edit cool-page && press Enter`,
},
{
content: "Enable old url redirect",
trigger: "#redirect_old_url_0",
run: "check",
},
{
content: "Open redirect type popup",
trigger: "#redirect_type_0",
run: "click"
},
{
content: "Set redirect type to temporary",
trigger: ".o-dropdown-item[data-choice-index='1']",
run: "click"
},
{
// TODO: this needs to be tested
content: "Change date published",
trigger: "#date_publish_0",
run: "edit 02/01/2005 01:00:00 && press enter",
},
{
content: "Don't index",
trigger: "#website_indexed_0",
run: "uncheck",
},
{
content: "Open visibility popup",
trigger: "#visibility_0",
run: "click",
},
{
// TODO: this needs to be tested
content: "Make visible with password only",
trigger: ".o-dropdown-item[data-choice-index='3']",
run: "click",
},
{
content: "Set password to 123",
trigger: "#visibility_password_display_0",
run: "edit 123",
},
{
content: "Make it a template",
trigger: "#is_new_page_template_0",
run: "check",
},
);
steps.check.push(
{
content: "Verify page title",
trigger: ":iframe head:hidden title:contains(/Cool Page/)",
},
...assertPageCanonicalUrlIs("/cool-page"),
stepUtils.goToUrl(getClientActionUrl("/new-page")),
assertPathName("/cool-page", "body"),
{
content: "Verify no index",
trigger: ':iframe head:hidden meta[name="robots"][content="noindex"]',
},
...checkIsTemplate(true, "Cool Page"),
);
steps.teardown.unshift(
{
content: "Reset page title",
trigger: "#name_0",
run: `edit New Page`,
},
{
content: `Change url back to /new-page`,
trigger: "#url_0",
run: `edit new-page && press Enter`,
},
{
content: "Open date published popup",
trigger: "#date_publish_0",
run: "click",
},
{
content: "Reset date published",
trigger: "button[title='Clear']",
run: "click",
},
{
content: "Do index",
trigger: "#website_indexed_0",
run: "check",
},
{
content: "Open visibility popup",
trigger: "#visibility_0",
run: "click",
},
{
content: "Make visible public",
trigger: ".o-dropdown-item[data-choice-index='0']",
run: "click",
},
{
content: "Remove from templates",
trigger: "#is_new_page_template_0",
run: "uncheck",
},
);
steps.checkTorndown.push(
{
content: "Verify page title",
trigger: ":iframe head:hidden title:contains(/New Page/)",
},
...assertPageCanonicalUrlIs("/new-page"),
stepUtils.goToUrl(getClientActionUrl("/new-page")),
assertPathName("/new-page", "body"),
{
content: "Verify is indexed",
trigger: ':iframe head:hidden:not(:has(meta[name="robots"][content="noindex"]))',
},
...checkIsTemplate(false),
);
return steps;
}
registerWebsitePreviewTour(
"website_page_properties_common",
{
url: "/test_view",
},
() => [...testCommonProperties("/test_view", false).finalize()],
);
registerWebsitePreviewTour(
"website_page_properties_can_publish",
{
url: "/test_website/model_item/1",
},
() => [...testCommonProperties("/test_website/model_item/1", true).finalize()],
);
registerWebsitePreviewTour(
"website_page_properties_website_page",
{
url: "/",
},
() => [
...openCreatePageDialog,
{
content: "Use blank template",
trigger: ".o_page_template .o_button_area:hidden",
run: "click",
},
{
content: "Name page",
trigger: ".modal-body input",
run: "edit New Page",
},
{
content: "Don't add to menu",
trigger: ".modal-body .o_switch",
run: "click",
},
{
content: "Click on Create button",
trigger: ".modal-footer .btn-primary",
run: "click",
},
{
content: "Wait for editor to open",
trigger: ":iframe body.editor_enable",
timeout: 30000,
},
...clickOnSave(),
...testWebsitePageProperties().finalize(),
],
);

View file

@ -1,49 +1,39 @@
/** @odoo-module */
import tour from "web_tour.tour";
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
const websiteName = "Website Test Settings";
tour.register("website_settings_m2o_dirty", {
test: true,
url: "/web",
},
[
tour.stepUtils.showAppsMenuItem(),
{
content: "open settings",
trigger: ".o_app[data-menu-xmlid='base.menu_administration'",
}, {
content: "open website settings",
trigger: ".settings_tab .tab[data-key='website']",
}, {
content: "check that the 'Shared Customers Accounts' setting is checked",
trigger: "input#shared_user_account:checked",
run: function () {}, // it's a check
}, {
content: "open website switcher",
trigger: "input#website_id",
}, {
content: `select ${websiteName} in the website switcher`,
trigger: `li:has(.dropdown-item:contains('${websiteName}'))`,
}, {
content: `check that the settings of ${websiteName} are loaded (Shared Customers Accounts)`,
trigger: "input#shared_user_account:not(:checked)",
run: function () {}, // it's a check
}, {
content: "click on the fake website setting after checking the edited website",
trigger: "button[name='action_website_test_setting']",
}, {
content: "check that we are on '/'",
trigger: "iframe body div#wrap",
run: function () {
if (window.location.pathname !== "/") {
// If this fails, it's probably because the change of website
// in the settings dirty the record and so there is a dialog
// save/discard displayed. This test ensure that does not happen
// because it makes actions unreachable in multi website.
console.error("We should be on '/' the settings didn't work");
}
}
},
]);
registry.category("web_tour.tours").add("website_settings_m2o_dirty", {
url: "/odoo",
steps: () => [
stepUtils.showAppsMenuItem(),
{
content: "open settings",
trigger: ".o_app[data-menu-xmlid='base.menu_administration']",
run: "click",
},
{
content: "open website settings",
trigger: ".settings_tab .tab[data-key='website']",
run: "click",
},
{
content: "check that the 'Shared Customers Accounts' setting is checked",
trigger: "input[id^='shared_user_account']:checked",
},
{
content: "open website switcher",
trigger: "input[id^='website_id']",
run: `edit ${websiteName}`,
},
{
content: `select ${websiteName} in the website switcher`,
trigger: `li:has(.dropdown-item:contains('${websiteName}'))`,
run: "click",
},
{
content: `check that the settings of ${websiteName} are loaded (Shared Customers Accounts)`,
trigger: "input[id^='shared_user_account']:not(:checked)",
},
],
});

View file

@ -4,6 +4,7 @@
from . import test_controller_args
from . import test_custom_snippet
from . import test_error
from . import test_form
from . import test_fuzzy
from . import test_image_upload_progress
from . import test_is_multilang
@ -13,9 +14,17 @@ from . import test_multi_company
from . import test_page_manager
from . import test_page
from . import test_performance
from . import test_qweb
from . import test_redirect
from . import test_reset_views
from . import test_restricted_editor
from . import test_session
from . import test_settings
from . import test_snippet_background_video
from . import test_systray
from . import test_theme_ir_asset
from . import test_translation
from . import test_views_during_module_operation
from . import test_website_controller_page
from . import test_website_page_properties
from . import test_website_field_sanitize

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<asset id="theme_default.test_asset_tag_aaa" name="Test asset tag (init active => keep active => update active)">
<bundle>test_asset_bundle</bundle>
<path>theme_default/tests/something.scss</path>
</asset>
<asset id="theme_default.test_asset_tag_aii" name="Test asset tag (init active => make inactive => update inactive)">
<bundle>test_asset_bundle</bundle>
<path>theme_default/tests/something.scss</path>
</asset>
<asset id="theme_default.test_asset_tag_aia" name="Test asset tag (init active => make inactive => update active)">
<bundle>test_asset_bundle</bundle>
<path>theme_default/tests/something.scss</path>
<field name="active">True</field> <!-- Take into account during update -->
</asset>
<asset id="theme_default.test_asset_tag_iii" name="Test asset tag (init inactive => keep inactive => update inactive)" active="False">
<bundle>test_asset_bundle</bundle>
<path>theme_default/tests/something.scss</path>
</asset>
<asset id="theme_default.test_asset_tag_iaa" name="Test asset tag (init inactive => make active => update active)" active="False">
<bundle>test_asset_bundle</bundle>
<path>theme_default/tests/something.scss</path>
</asset>
<asset id="theme_default.test_asset_tag_prepend" name="Test asset tag with directive">
<bundle directive="prepend">test_asset_bundle</bundle>
<path>theme_default/tests/something.scss</path>
</asset>
<asset id="theme_default.test_asset_tag_extra" name="Test asset tag with extra field">
<bundle>test_asset_bundle</bundle>
<path>theme_default/tests/something.scss</path>
<field name="sequence" eval="17"/>
</asset>
</odoo>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="test_website.test_template" name="test template 2">
&lt;!DOCTYPE html&gt;
<html>
<head>
<t t-call-assets="test_website.test_bundle" t-js="False"/>
<meta/>
<t t-call-assets="test_website.test_bundle" t-css="False"/>
</head>
<body>
<img src="http://test.external.link/img.png"/>
<img src="/test_website/static/img.png"/>
<a href="http://test.external.link/link">x</a>
<a href="/web/content/local_link">x</a>
<span t-attf-style="background-image: url('/web/image/2')" t-att-empty="False">xxx</span>
<div widget="html" t-field="user.signature"/>
<div widget="image" t-field="user.avatar_1920" t-options="{'widget': 'image'}"/>
</body>
</html>
</template>
<template id="test_website.test_template_tatt_qweb" name="t-att template">
<a t-att-href='"/"'>1</a>
<a t-att-href='False'>2</a>
<a t-att-href='None'>3</a>
<a t-att-href=''>4</a>
<a t-att-href='""'>5</a>
</template>
</odoo>

View file

@ -1,6 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo.tests
from odoo.tools import mute_logger
from unittest.mock import patch
@odoo.tests.common.tagged('post_install', '-at_install')
@ -32,9 +33,10 @@ class TestWebsiteControllerArgs(odoo.tests.HttpCase):
self.assertEqual(req.status_code, 200)
self.assertEqual(req.json(), {'a': 'valueA', 'kw': {'b': 'valueB'}})
req = self.url_open('/test_website/country/whatever-999999')
self.assertEqual(req.status_code, 404,
"Model converter record does not exist, return a 404.")
with patch.object(self.registry['ir.http'], '_get_error_html', lambda e, code, v: (code, '')):
req = self.url_open('/test_website/country/whatever-999999')
self.assertEqual(req.status_code, 404,
"Model converter record does not exist, return a 404.")
@odoo.tests.common.tagged('post_install', '-at_install')
@ -44,3 +46,8 @@ class TestWebsiteControllers(odoo.tests.TransactionCase):
website = self.env['website'].browse(1)
locs = website.with_user(website.user_id)._enumerate_pages(query_string='test_website_sitemap')
self.assertEqual(len(list(locs)), 1, "The same URL should only be shown once")
def test_02_search_controller(self):
website = self.env['website'].browse(1)
res = website._enumerate_pages(query_string="/test_website/country/elgium")
self.assertIn('/test_website/country/belgium', next(res).get('loc'))

View file

@ -0,0 +1,14 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged, HttpCase
@tagged('-at_install', 'post_install')
class TestForm(HttpCase):
def test_form_conditional_visibility_record_field(self):
self.start_tour(
self.env['website'].get_client_action_url('/test_website/model_item/1'),
'test_form_conditional_visibility_record_field',
login='admin',
)

View file

@ -5,7 +5,7 @@ import logging
import psycopg2
from odoo.addons.website.controllers.main import Website
from odoo.addons.website.tools import MockRequest
from odoo.addons.http_routing.tests.common import MockRequest
import odoo.tests
from odoo.tests.common import TransactionCase
@ -108,15 +108,15 @@ class TestAutoComplete(TransactionCase):
test_page.name = 'testTotallyUnique'
# Editor and Designer see pages in result
self._autocomplete_page('testTotallyUnique', 1, False)
self._autocomplete_page('testTotallyUnique', 1, None)
test_page.visibility = 'connected'
self._autocomplete_page('testTotallyUnique', 1, False)
test_page.visibility = False
test_page.groups_id = self.env.ref('base.group_public')
test_page.group_ids = self.env.ref('base.group_public')
self._autocomplete_page('testTotallyUnique', 1, False)
test_page.groups_id = False
test_page.group_ids = False
# Public user don't see restricted page
saved_env = self.env
@ -126,12 +126,12 @@ class TestAutoComplete(TransactionCase):
test_page.website_indexed = True
self._autocomplete_page('testTotallyUnique', 1, False)
test_page.groups_id = self.env.ref('base.group_system')
test_page.group_ids = self.env.ref('base.group_system')
self._autocomplete_page('testTotallyUnique', 0, "Not found")
test_page.groups_id = self.env.ref('base.group_public')
test_page.group_ids = self.env.ref('base.group_public')
self._autocomplete_page('testTotallyUnique', 1, False)
test_page.groups_id = False
test_page.group_ids = False
test_page.visibility = 'password'
self._autocomplete_page('testTotallyUnique', 0, "Not found")
@ -141,3 +141,11 @@ class TestAutoComplete(TransactionCase):
# restore website env for next tests
self.website.env = self.env = saved_env
def test_indirect(self):
self._autocomplete('module', 4, 'model')
self._autocomplete('rechord', 3, 'record')
self._autocomplete('suborder', 1, 'submodel')
# Sub-sub-fields are currently not supported.
# Adapt expected result if this becomes a feature.
self._autocomplete('tagg', 0, "Not found")

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.web_editor.controllers.main import Web_Editor
from odoo.addons.html_editor.controllers.main import HTML_Editor
from odoo.addons.web_unsplash.controllers.main import Web_Unsplash
import odoo.tests
@ -18,15 +18,15 @@ class TestImageUploadProgress(odoo.tests.HttpCase):
def test_02_image_upload_progress_unsplash(self):
BASE_URL = self.base_url()
@http.route('/web_editor/media_library_search', type='json', auth="user", website=True)
@http.route('/html_editor/media_library_search', type='jsonrpc', auth="user", website=True)
def media_library_search(self, **params):
return {"results": 0, "media": []}
# because not preprocessed by ControllerType metaclass
media_library_search.original_endpoint.routing_type = 'json'
# disable undraw, no third party should be called in tests
self.patch(Web_Editor, 'media_library_search', media_library_search)
self.patch(HTML_Editor, 'media_library_search', media_library_search)
@http.route("/web_unsplash/fetch_images", type='json', auth="user")
@http.route("/web_unsplash/fetch_images", type='jsonrpc', auth="user")
def fetch_unsplash_images(self, **post):
return {
'total': 1434,
@ -36,11 +36,11 @@ class TestImageUploadProgress(odoo.tests.HttpCase):
'alt_description': 'brown fox sitting on green grass field during daytime',
'urls': {
# 'regular': 'https://images.unsplash.com/photo-1462953491269-9aff00919695?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwzMDUwOHwwfDF8c2VhcmNofDF8fGZveHxlbnwwfHx8fDE2MzEwMzIzNDE&ixlib=rb-1.2.1&q=80&w=1080',
'regular': BASE_URL + '/website/static/src/img/phone.png',
'regular': BASE_URL + '/website/static/src/img/user-restricted-image.png',
},
'links': {
# 'download_location': 'https://api.unsplash.com/photos/HQqIOc8oYro/download?ixid=MnwzMDUwOHwwfDF8c2VhcmNofDF8fGZveHxlbnwwfHx8fDE2MzEwMzIzNDE'
'download_location': BASE_URL + '/website/static/src/img/phone.png',
'download_location': BASE_URL + '/website/static/src/img/user-restricted-image.png',
},
'user': {
'name': 'Mitchell Admin',

View file

@ -31,7 +31,7 @@ class TestIsMultiLang(odoo.tests.HttpCase):
it = self.env.ref('base.lang_it').sudo()
en = self.env.ref('base.lang_en').sudo()
be = self.env.ref('base.lang_fr_BE').sudo()
country1 = self.env['res.country'].create({'name': "My Super Country"})
country1 = self.env['res.country'].create({'name': "My Super Country", 'code': 'ZV'})
it.active = True
be.active = True

View file

@ -2,7 +2,6 @@
from lxml import html
from odoo.addons.website.tools import MockRequest
from odoo.tests import tagged, HttpCase
@ -21,6 +20,9 @@ class TestWebsiteMenu(HttpCase):
controller_url = '/test_website/model_item/'
website = self.env['website'].browse(1)
# First render to fill the cache.
self.url_open(f"{controller_url}{records[0].id}")
self.env['website.menu'].create([{
'name': records[0].name,
'url': f"{controller_url}{records[0].id}",
@ -36,10 +38,6 @@ class TestWebsiteMenu(HttpCase):
}])
for record in records:
record_url = f"{controller_url}{record.id}"
with MockRequest(self.env, website=website, url_root='', path=record_url):
tree = html.fromstring(self.env['ir.qweb']._render('test_website.model_item', {
'record': record,
'main_object': record,
}))
menu_link_el = tree.xpath(".//*[@id='top_menu']//a[@href='%s' and contains(@class, 'active')]" % record_url)
self.assertEqual(len(menu_link_el), 1, "The menu link related to the current record should be active")
tree = html.fromstring(self.url_open(record_url).content)
menu_link_el = tree.xpath(".//*[@id='top_menu']//a[@href='%s' and hasclass('active')]" % record_url)
self.assertEqual(len(menu_link_el), 1, "The menu link related to the current record should be active")

View file

@ -1,8 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import HttpCase, tagged
from odoo.tests.common import HOST
from odoo.tools import config, mute_logger
from odoo.tools import mute_logger
@tagged('-at_install', 'post_install')
@ -12,7 +11,7 @@ class WithContext(HttpCase):
website = self.env['website'].browse([1])
website.write({
'name': 'Test Website',
'domain': f'http://{HOST}:{config["http_port"]}',
'domain': self.base_url(),
'homepage_url': '/unexisting',
})
home_url = '/'

View file

@ -2,10 +2,11 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo.tests
from odoo.addons.website.tests.common import HttpCaseWithWebsiteUser
@odoo.tests.common.tagged('post_install', '-at_install')
class TestWebsitePageManager(odoo.tests.HttpCase):
class TestWebsitePageManager(HttpCaseWithWebsiteUser):
def test_page_manager_test_model(self):
if self.env['website'].search_count([]) == 1:
website2 = self.env['website'].create({
@ -21,8 +22,8 @@ class TestWebsitePageManager(odoo.tests.HttpCase):
"There should at least be one record without website_id and one for 2 different websites",
)
self.assertNotIn('website_id', self.env['test.model']._fields)
self.start_tour('/web#action=test_website.action_test_model_multi_website', 'test_website_page_manager', login="admin")
self.start_tour('/odoo/action-test_website.action_test_model_multi_website', 'test_website_page_manager', login="website_user")
# This second test is about ensuring that you can switch from a list
# view which has no `website_pages_list` js_class to its kanban view
self.start_tour('/web#action=test_website.action_test_model_multi_website_js_class_bug', 'test_website_page_manager_js_class_bug', login="admin")
self.start_tour('/web#action=test_website.action_test_model', 'test_website_page_manager_no_website_id', login="admin")
self.start_tour('/odoo/action-test_website.action_test_model_multi_website_js_class_bug', 'test_website_page_manager_js_class_bug', login="website_user")
self.start_tour('/odoo/action-test_website.action_test_model', 'test_website_page_manager_no_website_id', login="website_user")

View file

@ -8,7 +8,6 @@ class TestPerformance(UtilPerf):
def test_10_perf_sql_website_controller_minimalist(self):
url = '/empty_controller_test'
select_tables_perf = {
'base_registry_signaling': 1,
'orm_signaling_registry': 1,
}
self._check_url_hot_query(url, 1, select_tables_perf)
self.assertEqual(self._get_url_hot_query(url, cache=False), 1)

View file

@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from lxml import etree
import re
from odoo import tools
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
from odoo.addons.http_routing.tests.common import MockRequest
class TestQweb(TransactionCaseWithUserDemo):
def _load(self, module, filepath):
tools.convert_file(self.env, module, filepath, {}, 'init')
def test_qweb_cdn(self):
self._load('test_website', 'tests/template_qweb_test.xml')
website = self.env.ref('website.default_website')
website.write({
"cdn_activated": True,
"cdn_url": "http://test.cdn"
})
demo = self.env['res.users'].search([('login', '=', 'demo')])[0]
demo.write({"signature": '''<span class="toto">
span<span class="fa"></span><img src="/web/image/1"/>
</span>'''})
demo_env = self.env(user=demo)
html = demo_env['ir.qweb']._render('test_website.test_template', {"user": demo}, website_id=website.id)
asset_bundle_xmlid = 'test_website.test_bundle'
qweb = self.env['ir.qweb']
bundle = qweb._get_asset_bundle(asset_bundle_xmlid, css=True, js=True, assets_params={'website_id': website.id})
asset_version_js = bundle.get_version('js')
asset_version_css = bundle.get_version('css')
css_url, js_url = bundle.get_links()[-2:]
html = html.strip()
html = re.sub(r'\?unique=[^"]+', '', html).encode('utf8')
format_data = {
"css": css_url,
"js": js_url,
"user_id": demo.id,
"filename": "Marc%20Demo",
"alt": "Marc Demo",
"asset_xmlid": asset_bundle_xmlid,
"asset_version_css": asset_version_css,
"asset_version_js": asset_version_js,
}
self.assertHTMLEqual(html, ("""<!DOCTYPE html>
<html>
<head>
<link type="text/css" rel="stylesheet" href="http://test.external.link/style1.css"/>
<link type="text/css" rel="stylesheet" href="http://test.external.link/style2.css"/>
<link type="text/css" rel="stylesheet" href="http://test.cdn%(css)s"/>
<meta/>
<script type="text/javascript" src="http://test.external.link/javascript1.js"></script>
<script type="text/javascript" src="http://test.external.link/javascript2.js"></script>
<script type="text/javascript" src="http://test.cdn%(js)s" onerror="__odooAssetError=1"></script>
</head>
<body>
<img src="http://test.external.link/img.png" loading="lazy"/>
<img src="http://test.cdn/test_website/static/img.png" loading="lazy"/>
<a href="http://test.external.link/link">x</a>
<a href="http://test.cdn/web/content/local_link">x</a>
<span style="background-image: url(&#39;http://test.cdn/web/image/2&#39;)">xxx</span>
<div widget="html"><span class="toto">
span<span class="fa"></span><img src="http://test.cdn/web/image/1" loading="lazy">
</span></div>
<div widget="image"><img src="http://test.cdn/web/image/res.users/%(user_id)s/avatar_1920/%(filename)s" class="img img-fluid" alt="%(alt)s" loading="lazy"/></div>
</body>
</html>""" % format_data).encode('utf8'))
with MockRequest(self.env, website=website):
html = demo_env['ir.qweb']._render('test_website.test_template_tatt_qweb', {}, website_id=website.id)
self.assertHTMLEqual(html, ("""
<html>
<body><a href="/">1</a>
<a>2</a>
<a>3</a>
<a>4</a>
<a href="">5</a></body>
</html>
"""))

View file

@ -3,7 +3,6 @@
import odoo
from odoo.tests import HttpCase, tagged
from odoo.tools import mute_logger
from odoo.addons.http_routing.models.ir_http import slug
from unittest.mock import patch
@ -19,7 +18,7 @@ class TestRedirect(HttpCase):
'login': 'portal_user',
'password': 'portal_user',
'email': 'portal_user@mail.com',
'groups_id': [(6, 0, [self.env.ref('base.group_portal').id])]
'group_ids': [(6, 0, [self.env.ref('base.group_portal').id])]
})
def test_01_redirect_308_model_converter(self):
@ -37,7 +36,7 @@ class TestRedirect(HttpCase):
- Correct & working redirect as logged in user
- Correct replace of url_for() URLs in DOM
"""
url = '/test_website/country/' + slug(country_ad)
url = '/test_website/country/' + self.env['ir.http']._slug(country_ad)
redirect_url = url.replace('test_website', 'redirected')
# [Public User] Open the original url and check redirect OK
@ -57,12 +56,12 @@ class TestRedirect(HttpCase):
self.assertTrue(redirect_url in r.text, "Ensure the url_for has replaced the href URL in the DOM")
def test_redirect_308_by_method_url_rewrite(self):
self.env['website.rewrite'].create({
self.env['website.rewrite'].create([{
'name': 'Test Website Redirect',
'redirect_type': '308',
'url_from': url_from,
'url_to': f'{url_from}_new',
} for url_from in ('/get', '/post', '/get_post'))
} for url_from in ('/get', '/post', '/get_post')])
self.env.ref('test_website.test_view').arch = '''
<t>
@ -90,7 +89,7 @@ class TestRedirect(HttpCase):
rec_published = self.env['test.model'].create({'name': 'name', 'website_published': True})
rec_unpublished = self.env['test.model'].create({'name': 'name', 'website_published': False})
WebsiteHttp = odoo.addons.website.models.ir_http.Http
WebsiteHttp = odoo.addons.website.models.ir_http.IrHttp
def _get_error_html(env, code, value):
return str(code).split('_')[-1], f"CUSTOM {code}"
@ -201,7 +200,7 @@ class TestRedirect(HttpCase):
'name': '301 test record',
'is_published': True,
})
url_rec1 = '/test_website/200/' + slug(rec1)
url_rec1 = '/test_website/200/' + self.env['ir.http']._slug(rec1)
r = self.url_open(url_rec1)
self.assertEqual(r.status_code, 200)
@ -223,7 +222,7 @@ class TestRedirect(HttpCase):
# 4. Accessing unpublished record with redirect to another published
# record: expecting redirect to that record
rec2 = rec1.copy({'is_published': True})
url_rec2 = '/test_website/200/' + slug(rec2)
url_rec2 = '/test_website/200/' + self.env['ir.http']._slug(rec2)
redirect.url_to = url_rec2
r = self.url_open(url_rec1)
self.assertEqual(r.status_code, 200)
@ -264,7 +263,7 @@ class TestRedirect(HttpCase):
'name': '301 test record',
'is_published': True,
})
url_rec1 = f"/test_countries_308/{slug(rec1)}"
url_rec1 = f"/test_countries_308/{self.env['ir.http']._slug(rec1)}"
resp = self.url_open("/test_countries_308", allow_redirects=False)
self.assertEqual(resp.status_code, 308)

View file

@ -1,8 +1,11 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import re
import odoo.tests
from odoo.tools import mute_logger
import unittest
def break_view(view, fr='<p>placeholder</p>', to='<p t-field="no_record.exist"/>'):
@ -17,8 +20,10 @@ class TestWebsiteResetViews(odoo.tests.HttpCase):
resp = self.url_open(page)
self.assertEqual(resp.status_code, 500, "Waiting 500")
self.assertTrue('<button data-mode="soft" class="reset_templates_button' in resp.text)
data = {'view_id': self.find_template(resp), 'redirect': page, 'mode': mode}
resp = self.url_open('/website/reset_template', data)
data = {'params': {'view_id': self.find_template(resp), 'mode': mode}}
self.url_open('/website/reset_template', data=json.dumps(data), headers={'Content-Type': 'application/json'})
resp = self.url_open(page)
self.assertTrue(resp.url.endswith(page), "We should be checking the test page")
self.assertEqual(resp.status_code, 200, "Waiting 200")
def find_template(self, response):
@ -104,6 +109,7 @@ class TestWebsiteResetViews(odoo.tests.HttpCase):
# Break it again to have a previous arch different than file arch
break_view(self.test_page_view.with_context(website_id=1))
self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view")
with self.assertRaises(AssertionError):
# soft reset should not be able to reset the view as previous
# version is also broken

View file

@ -0,0 +1,52 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import unittest
import odoo.tests
from odoo.tools import mute_logger
from odoo.addons.website.tests.common import HttpCaseWithWebsiteUser
@odoo.tests.common.tagged('post_install', '-at_install')
class TestRestrictedEditor(HttpCaseWithWebsiteUser):
@classmethod
def setUpClass(cls):
super().setUpClass()
website = cls.env['website'].search([], limit=1)
fr = cls.env.ref('base.lang_fr').sudo()
en = cls.env.ref('base.lang_en').sudo()
fr.active = True
website.default_lang_id = en
website.language_ids = en + fr
cls.env['website.menu'].create({
'name': 'Model item',
'url': '/test_website/model_item/1',
'parent_id': website.menu_id.id,
'sequence': 100,
})
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
def test_01_restricted_editor_only(self):
self.start_tour(self.env['website'].get_client_action_url('/'), 'test_restricted_editor_only', login="website_user")
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
def test_02_restricted_editor_test_admin(self):
self.user_website_user.group_ids += self.env.ref("test_website.group_test_website_admin")
self.start_tour(self.env['website'].get_client_action_url('/'), 'test_restricted_editor_test_admin', login="website_user")
# FIXME the logic of the commit that introduced the fix at 8c41c147a4c6a415e
# was reverted, so this test is disabled for now. Branding *on views* as
# a restricted editor is something we want in some custo (e.g. odoo.com).
# See commit messages for details.
@unittest.skip
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
def test_03_restricted_editor_tester(self):
"""
Tests that restricted users cannot edit ir.ui.view records despite being
on a page of a record (main_object) they can edit.
"""
self.user_website_user.group_ids += self.env.ref("test_website.group_test_website_tester")
self.start_tour(self.env['website'].get_client_action_url('/test_model/1'), 'test_restricted_editor_tester', login='website_user')

View file

@ -1,7 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from lxml import html
from unittest.mock import patch
from odoo import http
from odoo.addons.website.models.website import Website
import odoo.tests
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
@ -13,27 +16,77 @@ class TestWebsiteSession(HttpCaseWithUserDemo):
def test_01_run_test(self):
self.start_tour('/', 'test_json_auth')
def test_02_inactive_session_lang(self):
session = self.authenticate(None, None)
self.env.ref('base.lang_fr').active = False
session.context['lang'] = 'fr_FR'
odoo.http.root.session_store.save(session)
# ensure that _get_current_website_id will be able to match a website
current_website_id = self.env["website"]._get_current_website_id(odoo.tests.HOST)
self.env["website"].browse(current_website_id).domain = odoo.tests.HOST
res = self.url_open('/test_website_sitemap') # any auth='public' route would do
res.raise_for_status()
def test_03_totp_login_with_inactive_session_lang(self):
session = self.authenticate(None, None)
self.env.ref('base.lang_fr').active = False
session.context['lang'] = 'fr_FR'
odoo.http.root.session_store.save(session)
# ensure that _get_current_website_id will be able to match a website
current_website_id = self.env["website"]._get_current_website_id(odoo.tests.HOST)
self.env["website"].browse(current_website_id).domain = odoo.tests.HOST
with patch.object(self.env.registry["res.users"], "_mfa_url", return_value="/web/login/totp"):
res = self.url_open('/web/login', allow_redirects=False, data={
'login': 'demo',
'password': 'demo',
'csrf_token': http.Request.csrf_token(self),
})
res.raise_for_status()
self.assertEqual(res.status_code, 303)
self.assertTrue(res.next.path_url.startswith("/web/login/totp"))
def test_04_ensure_website_get_cached_values_can_be_called(self):
session = self.authenticate('admin', 'admin')
# Force a browser language that is not installed
session.context['lang'] = 'fr_MC'
http.root.session_store.save(session)
# Disable cache in order to make sure that values would be fetched at any time
get_cached_values_without_cache = Website._get_cached_values.__cache__.method
with patch.object(Website, '_get_cached_values',
side_effect=get_cached_values_without_cache, autospec=True):
# ensure that permissions on logout are OK
res = self.url_open('/web/session/logout')
self.assertEqual(res.status_code, 200)
def test_branding_cache(self):
def has_branding(html_text):
el = html.fromstring(html_text)
return el.xpath('//*[@data-oe-model="test.model"]')
self.user_demo.groups_id += self.env.ref('website.group_website_restricted_editor')
self.user_demo.groups_id -= self.env.ref('website.group_website_designer')
self.user_demo.group_ids += self.env.ref('website.group_website_restricted_editor')
self.user_demo.group_ids += self.env.ref('test_website.group_test_website_admin')
self.user_demo.group_ids -= self.env.ref('website.group_website_designer')
# Create session for demo user.
public_session = self.authenticate(None, None)
demo_session = self.authenticate('demo', 'demo')
record = self.env['test.model'].search([], limit=1)
result = self.url_open(f'/test_website/model_item/{record.id}')
result = self.url_open(f'/test_website/model_item_sudo/{record.id}')
self.assertTrue(has_branding(result.text), "Should have branding for user demo")
# Public user.
self.opener.cookies['session_id'] = public_session.sid
result = self.url_open(f'/test_website/model_item/{record.id}')
self.opener.cookies.set("session_id", public_session.sid, domain=odoo.tests.common.HOST)
result = self.url_open(f'/test_website/model_item_sudo/{record.id}')
self.assertFalse(has_branding(result.text), "Should have no branding for public user")
# Back to demo user.
self.opener.cookies['session_id'] = demo_session.sid
result = self.url_open(f'/test_website/model_item/{record.id}')
self.opener.cookies.set("session_id", demo_session.sid, domain=odoo.tests.common.HOST)
result = self.url_open(f'/test_website/model_item_sudo/{record.id}')
self.assertTrue(has_branding(result.text), "Should have branding for user demo")

View file

@ -6,6 +6,7 @@ import odoo.tests
@odoo.tests.tagged('-at_install', 'post_install')
class TestWebsiteSettings(odoo.tests.HttpCase):
def test_01_multi_website_settings(self):
# If not enabled (like in demo data), landing on res.config will try
# to disable module_sale_quotation_builder and raise an issue
@ -13,4 +14,4 @@ class TestWebsiteSettings(odoo.tests.HttpCase):
if group_order_template:
self.env.ref('base.group_user').write({"implied_ids": [(4, group_order_template.id)]})
self.env['website'].create({'name': "Website Test Settings", 'specific_user_account': True})
self.start_tour("/web", 'website_settings_m2o_dirty', login="admin")
self.start_tour("/odoo", 'website_settings_m2o_dirty', login="admin")

View file

@ -0,0 +1,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo.tests
@odoo.tests.common.tagged('post_install', '-at_install')
class TestSnippetBackgroundVideo(odoo.tests.HttpCase):
def test_snippet_background_video(self):
self.start_tour("/", "snippet_background_video", login="admin")

View file

@ -1,3 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import HOST, new_test_user, tagged
@ -31,7 +32,8 @@ class TestSystray(HttpCase):
</xpath>
"""
})
# Remain on page when switching website
cls.env['website'].search([]).homepage_url = '/test_model/1'
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
def test_01_admin(self):
@ -39,27 +41,43 @@ class TestSystray(HttpCase):
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
def test_02_reditor_tester(self):
self.user_test.groups_id |= self.group_restricted_editor
self.user_test.groups_id |= self.group_tester
self.user_test.group_ids |= self.group_restricted_editor
self.user_test.group_ids |= self.group_tester
self.start_tour(self.env['website'].get_client_action_url('/test_model/1'), 'test_systray_reditor_tester', login="testtest")
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
def test_03_reditor_not_tester(self):
self.user_test.groups_id |= self.group_restricted_editor
self.user_test.groups_id = self.user_test.groups_id.filtered(lambda group: group != self.group_tester)
self.assertNotIn(self.group_tester.id, self.user_test.groups_id.ids, "User should not be a group_tester")
self.user_test.group_ids |= self.group_restricted_editor
self.user_test.group_ids = self.user_test.group_ids.filtered(lambda group: group != self.group_tester)
self.assertNotIn(self.group_tester.id, self.user_test.group_ids.ids, "User should not be a group_tester")
self.start_tour(self.env['website'].get_client_action_url('/test_model/1'), 'test_systray_reditor_not_tester', login="testtest")
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
def test_04_not_reditor_tester(self):
self.user_test.groups_id = self.user_test.groups_id.filtered(lambda group: group != self.group_restricted_editor)
self.user_test.groups_id |= self.group_tester
self.assertNotIn(self.group_restricted_editor.id, self.user_test.groups_id.ids, "User should not be a group_restricted_editor")
self.user_test.group_ids = self.user_test.group_ids.filtered(lambda group: group != self.group_restricted_editor)
self.user_test.group_ids |= self.group_tester
self.assertNotIn(self.group_restricted_editor.id, self.user_test.group_ids.ids, "User should not be a group_restricted_editor")
self.start_tour(self.env['website'].get_client_action_url('/test_model/1'), 'test_systray_not_reditor_tester', login="testtest")
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
def test_05_not_reditor_not_tester(self):
self.user_test.groups_id = self.user_test.groups_id.filtered(lambda group: group not in [self.group_restricted_editor, self.group_tester])
self.assertNotIn(self.group_restricted_editor.id, self.user_test.groups_id.ids, "User should not be a group_restricted_editor")
self.assertNotIn(self.group_tester.id, self.user_test.groups_id.ids, "User should not be a group_tester")
self.user_test.group_ids = self.user_test.group_ids.filtered(lambda group: group not in [self.group_restricted_editor, self.group_tester])
self.assertNotIn(self.group_restricted_editor.id, self.user_test.group_ids.ids, "User should not be a group_restricted_editor")
self.assertNotIn(self.group_tester.id, self.user_test.group_ids.ids, "User should not be a group_tester")
self.start_tour(self.env['website'].get_client_action_url('/test_model/1'), 'test_systray_not_reditor_not_tester', login="testtest")
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
def test_06_single_website(self):
if self.env['website'].search_count([]) > 1:
website = self.env['website'].search([], limit=1)
websites_to_remove = self.env['website'].search([('id', '!=', website.id)])
websites_to_remove.unlink()
self.start_tour(self.env['website'].get_client_action_url('/test_model/1'), 'test_systray_single_website', login="admin")
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
def test_07_multi_website(self):
if self.env['website'].search_count([]) == 1:
self.env['website'].create({
'name': 'My Website 2',
})
self.start_tour(self.env['website'].get_client_action_url('/test_model/1'), 'test_systray_multi_website', login="admin")

View file

@ -0,0 +1,77 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase, tagged
from odoo.tools import convert_file
from odoo.tools.misc import file_path
@tagged('-at_install', 'post_install')
class TestThemeAsset(TransactionCase):
def test_theme_asset_tag(self):
"""
Verify that assets defined with the <asset> tag are properly imported.
"""
# Load new records
convert_file(
self.env, 'theme_default',
file_path('test_website/tests/asset_tag.xml'),
{}, 'init', False,
)
active_keep_asset = self.env.ref('theme_default.test_asset_tag_aaa')
inactive_keep_asset = self.env.ref('theme_default.test_asset_tag_iii')
active_switch_asset_reset = self.env.ref('theme_default.test_asset_tag_aia')
active_switch_asset_ignore = self.env.ref('theme_default.test_asset_tag_aii')
inactive_switch_asset = self.env.ref('theme_default.test_asset_tag_iaa')
prepend_asset = self.env.ref('theme_default.test_asset_tag_prepend')
asset_with_extra_field = self.env.ref('theme_default.test_asset_tag_extra')
# Verify initial load
self.assertEqual(prepend_asset._name, 'theme.ir.asset', 'Model should be theme.ir.asset')
self.assertEqual(prepend_asset.name, 'Test asset tag with directive', 'Name not loaded')
self.assertEqual(prepend_asset.directive, 'prepend', 'Directive not loaded')
self.assertEqual(prepend_asset.bundle, 'test_asset_bundle', 'Bundle not loaded')
self.assertEqual(prepend_asset.path, 'theme_default/tests/something.scss', 'Path not loaded')
self.assertEqual(asset_with_extra_field.sequence, 17, 'Sequence not loaded')
self.assertTrue(active_keep_asset.active, 'Should be active')
self.assertTrue(active_switch_asset_reset.active, 'Should be active')
self.assertTrue(active_switch_asset_ignore.active, 'Should be active')
self.assertFalse(inactive_keep_asset.active, 'Should be inactive')
self.assertFalse(inactive_switch_asset.active, 'Should be inactive')
# Patch records
prepend_asset.name = 'changed'
prepend_asset.directive = 'append'
prepend_asset.bundle = 'changed'
prepend_asset.path = 'theme_default/tests/changed.scss'
asset_with_extra_field.sequence = 3
active_switch_asset_reset.active = False
active_switch_asset_ignore.active = False
inactive_switch_asset.active = True
# Update records
convert_file(
self.env, 'theme_default',
file_path('test_website/tests/asset_tag.xml'),
{
'theme_default.test_asset_tag_aaa': active_keep_asset.id,
'theme_default.test_asset_tag_iii': inactive_keep_asset.id,
'theme_default.test_asset_tag_aia': active_switch_asset_reset.id,
'theme_default.test_asset_tag_aii': active_switch_asset_ignore.id,
'theme_default.test_asset_tag_iaa': inactive_switch_asset.id,
'theme_default.test_asset_tag_prepend': prepend_asset.id,
'theme_default.test_asset_tag_extra': asset_with_extra_field.id,
}, 'update', False,
)
# Verify updated load
self.assertEqual(prepend_asset.name, 'Test asset tag with directive', 'Name not restored')
self.assertEqual(prepend_asset.directive, 'prepend', 'Directive not restored')
self.assertEqual(prepend_asset.bundle, 'test_asset_bundle', 'Bundle not restored')
self.assertEqual(prepend_asset.path, 'theme_default/tests/something.scss', 'Path not restored')
self.assertEqual(asset_with_extra_field.sequence, 17, 'Sequence not restored')
self.assertTrue(active_keep_asset.active, 'Should be active')
self.assertTrue(active_switch_asset_reset.active, 'Should be reset to active')
self.assertFalse(active_switch_asset_ignore.active, 'Should be kept inactive')
self.assertFalse(inactive_keep_asset.active, 'Should be inactive')
self.assertTrue(inactive_switch_asset.active, 'Should be kept active')

View file

@ -0,0 +1,129 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.tests import HttpCase, tagged
@tagged('post_install', '-at_install', 'is_tour')
class TestTranslation(HttpCase):
def _single_language_fr_user_fr_site(self):
self.start_tour(self.env['website'].get_client_action_url('/'), 'translation_single_language_fr_user_fr_site', login='admin')
def _single_language_en_user_fr_site(self):
self.start_tour(self.env['website'].get_client_action_url('/'), 'translation_single_language_en_user_fr_site', login='admin')
def _single_language_fr_user_en_site(self):
self.start_tour(self.env['website'].get_client_action_url('/'), 'translation_single_language_fr_user_en_site', login='admin')
def _multi_language_fr_user_fr_en_site(self):
self.start_tour(self.env['website'].get_client_action_url('/'), 'translation_multi_language_fr_user_fr_en_site', login='admin', timeout=250)
def _multi_language_fr_user_en_fr_site(self):
self.start_tour(self.env['website'].get_client_action_url('/'), 'translation_multi_language_fr_user_en_fr_site', login='admin', timeout=250)
def _multi_language_en_user_fr_en_site(self):
self.start_tour(self.env['website'].get_client_action_url('/'), 'translation_multi_language_en_user_fr_en_site', login='admin', timeout=250)
def _multi_language_en_user_en_fr_site(self):
self.start_tour(self.env['website'].get_client_action_url('/'), 'translation_multi_language_en_user_en_fr_site', login='admin', timeout=250)
def _fr_db(self):
self._fr_en_db()
lang_en = self.env.ref('base.lang_en')
lang_en.active = False
def _fr_en_db(self):
lang_en = self.env.ref('base.lang_en')
lang_fr = self.env.ref('base.lang_fr')
self.env["base.language.install"].create({
'overwrite': True,
'lang_ids': [(6, 0, [lang_fr.id])],
}).lang_install()
for website in self.env['website'].search([]):
website.language_ids += lang_fr
website.default_lang_id = lang_fr
website.language_ids -= lang_en
self.env['website'].create({
'sequence': 1,
'name': 'Test FR Website',
'language_ids': [
Command.link(lang_fr.id),
],
'default_lang_id': lang_fr.id,
})
for user in self.env['res.users'].search([]):
user.lang = lang_fr.code
for partner in self.env['res.partner'].search([]):
partner.lang = lang_fr.code
for user in self.env['res.users'].with_context(active_test=False).search([]):
user.lang = lang_fr.code
def _en_fr_db(self):
lang_fr = self.env.ref('base.lang_fr')
self.env["base.language.install"].create({
'overwrite': True,
'lang_ids': [(6, 0, [lang_fr.id])],
}).lang_install()
def test_fr_db_fr_site(self):
self._fr_db()
self._single_language_fr_user_fr_site()
def test_fr_en_db_fr_site(self):
self._fr_en_db()
self._single_language_fr_user_fr_site()
def test_fr_en_db_en_site(self):
self._fr_en_db()
lang_en = self.env.ref('base.lang_en')
lang_fr = self.env.ref('base.lang_fr')
for website in self.env['website'].search([]):
website.language_ids += lang_en
website.default_lang_id = lang_en
website.language_ids -= lang_fr
self._single_language_fr_user_en_site()
def test_fr_en_db_fr_en_site(self):
self._fr_en_db()
lang_en = self.env.ref('base.lang_en')
for website in self.env['website'].search([]):
website.language_ids += lang_en
self._multi_language_fr_user_fr_en_site()
def test_fr_en_db_en_fr_site(self):
self._fr_en_db()
lang_en = self.env.ref('base.lang_en')
for website in self.env['website'].search([]):
website.language_ids += lang_en
website.default_lang_id = lang_en
self._multi_language_fr_user_en_fr_site()
def test_en_fr_db_fr_site(self):
self._en_fr_db()
lang_en = self.env.ref('base.lang_en')
lang_fr = self.env.ref('base.lang_fr')
self.env["base.language.install"].create({
'overwrite': True,
'lang_ids': [(6, 0, [lang_fr.id])],
}).lang_install()
for website in self.env['website'].search([]):
website.language_ids += lang_fr
website.default_lang_id = lang_fr
website.language_ids -= lang_en
self._single_language_en_user_fr_site()
def test_en_fr_db_fr_en_site(self):
self._en_fr_db()
lang_fr = self.env.ref('base.lang_fr')
for website in self.env['website'].search([]):
website.language_ids += lang_fr
website.default_lang_id = lang_fr
self._multi_language_en_user_fr_en_site()
def test_en_fr_db_en_fr_site(self):
self._en_fr_db()
lang_fr = self.env.ref('base.lang_fr')
for website in self.env['website'].search([]):
website.language_ids += lang_fr
self._multi_language_en_user_en_fr_site()

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.website.tools import MockRequest
from odoo.addons.http_routing.tests.common import MockRequest
from odoo.tests import standalone
@ -70,8 +70,7 @@ def test_01_cow_views_unlink_on_module_update(env):
# Upgrade the module
test_website_module = env['ir.module.module'].search([('name', '=', 'test_website')])
test_website_module.button_immediate_upgrade()
env.reset() # clear the set of environments
env = env() # get an environment that refers to the new registry
env.transaction.reset() # clear the set of environments
# Ensure generic views got removed
view = env.ref('test_website.update_module_view_to_be_t_called', raise_if_not_found=False)
@ -178,10 +177,22 @@ def test_02_copy_ids_views_unlink_on_module_update(env):
view_website_1, view_website_2, theme_child_view = _simulate_xml_view()
old_registry = env.registry
# Upgrade the module
theme_default.button_immediate_upgrade()
env.reset() # clear the set of environments
env = env() # get an environment that refers to the new registry
env.transaction.reset() # clear the set of environments
# Beware: records do not belong to the correct registry anymore
assert env.registry is not old_registry
# Therefore we need to re-obtain them
View = env['ir.ui.view']
ThemeView = env['theme.ir.ui.view']
Imd = env['ir.model.data']
website_1 = env['website'].browse(1)
website_2 = env['website'].browse(2)
theme_default = env.ref('base.module_theme_default')
# Ensure the theme.ir.ui.view got removed (since there is an IMD but not
# present in XML files)
@ -204,8 +215,7 @@ def test_02_copy_ids_views_unlink_on_module_update(env):
# Upgrade the module
with MockRequest(env, website=website_1):
theme_default.button_immediate_upgrade()
env.reset() # clear the set of environments
env = env() # get an environment that refers to the new registry
env.transaction.reset() # clear the set of environments
# Ensure the theme.ir.ui.view got removed (since there is an IMD but not
# present in XML files)

View file

@ -0,0 +1,195 @@
from lxml import html
from odoo.tools import mute_logger
from odoo.exceptions import AccessError, ValidationError
from odoo.tests import HttpCase, tagged
import unittest
from odoo.addons.website.controllers.model_page import ModelPageController
@tagged('post_install', '-at_install')
class TestWebsiteControllerPage(HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.model = cls.env["ir.model"]._get("test.model.exposed")
cls.model_acl = cls.env["ir.model.access"].create({
"name": "test acl expose",
"model_id": cls.model.id,
"group_id": cls.env.ref("website.website_page_controller_expose").id,
"perm_read": True,
})
cls.listing_view = cls.env["ir.ui.view"].create({
"type": "qweb",
"model": cls.model.model,
"arch": """<t t-call="website.layout">
<t t-set="_activeClasses" t-translation="off">border-primary</t>
<div t-attf-class="listing_layout_switcher btn-group ms-3" t-att-data-active-classes="_activeClasses" t-att-data-view-id="view_id">
<input type="radio" class="btn-check" name="wstudio_layout" id="o_wstudio_apply_grid" value="grid" t-att-checked="'checked' if layout_mode != 'list' else None"/>
<label t-attf-class="btn btn-light #{_activeClasses if layout_mode != 'list' else None} o_wstudio_apply_grid" title="Grid" for="o_wstudio_apply_grid">
<i class="fa fa-th-large"/>
</label>
<input type="radio" class="btn-check" name="wstudio_layout" id="o_wstudio_apply_list" t-att-checked="'checked' if layout_mode == 'list' else None" value="list"/>
<label t-attf-class="btn btn-light #{_activeClasses if layout_mode == 'list' else None} o_wstudio_apply_list" title="List" for="o_wstudio_apply_list">
<i class="oi oi-view-list"/>
</label>
</div>
<div t-attf-class="row mx-n2 mt8 #{'o_website_grid' if layout_mode == 'grid' else 'o_website_list'}">
<t t-foreach="records" t-as="record">
<a class="test_record_listing" t-out="record.display_name" t-att-href="record_to_url(record)" />
</t>
</div>
</t> """
})
cls.single_view = cls.env["ir.ui.view"].create({
"type": "qweb",
"model": cls.model.model,
"arch": """<t t-call="website.layout">
<div class="test_record" t-out="record.display_name" />
</t> """
})
cls.listing_controller_page = cls.env["website.controller.page"].create({
"name": "Exposed Model",
"view_id": cls.listing_view.id,
"record_view_id": cls.single_view.id,
"record_domain": "[('name', '=ilike', 'test_partner_%')]",
"website_published": True,
})
partner_data = {}
if "is_published" in cls.env[cls.model.model]._fields:
partner_data["is_published"] = True
records_to_create = [dict(name=f"test_partner_{i}", **partner_data) for i in range(2)]
cls.exposed_records = cls.env[cls.model.model].create(records_to_create)
def test_cannot_bypass_read_rights(self):
self.env["ir.model.access"].search([("model_id", "=", self.model.id)]).perm_read = False
with self.assertRaises(AccessError) as cm:
self.env["website.controller.page"].with_user(2).create({
"name": "Exposed Model Read",
"website_id": False,
"view_id": self.single_view.id,
"record_domain": "[('name', '=ilike', 'test_partner_%')]",
"website_published": True,
})
self.assertEqual(str(cm.exception).split("\n")[0], "You are not allowed to access 'Website Model Test Exposed' (test.model.exposed) records.")
def test_access_rights_and_rules(self):
self.authenticate(None, None)
self.model_acl.active = False
with mute_logger("odoo.http"):
response = self.url_open(f"/model/{self.listing_controller_page.name_slugified}")
self.assertEqual(response.status_code, 403)
self.model_acl.active = True
response = self.url_open(f"/model/{self.listing_controller_page.name_slugified}")
self.assertEqual(response.status_code, 200)
tree = html.fromstring(response.content.decode())
rec_nodes = tree.xpath("//a[@class='test_record_listing']")
self.assertEqual(len(rec_nodes), 2)
self.env["ir.rule"].create({
"name": "dummy",
"model_id": self.model.id,
"domain_force": "[('name', '=', 'test_partner_1')]",
"groups": self.env.ref("base.group_public"),
})
response = self.url_open(f"/model/{self.listing_controller_page.name_slugified}")
tree = html.fromstring(response.content.decode())
rec_nodes = tree.xpath("//a[@class='test_record_listing']")
self.assertEqual(len(rec_nodes), 1)
def test_expose_model(self):
self.authenticate(None, None)
slug = self.env['ir.http']._slug
response = self.url_open(f"/model/{self.listing_controller_page.name_slugified}")
tree = html.fromstring(response.content.decode())
rec_nodes = tree.xpath("//a[@class='test_record_listing']")
self.assertEqual(len(rec_nodes), 2)
for n, record in zip(rec_nodes, self.exposed_records):
self.assertEqual(n.get("href"), f"/model/{self.listing_controller_page.name_slugified}/{slug(record)}")
response = self.url_open(f"/model/{self.listing_controller_page.name_slugified}/{slug(self.exposed_records[0])}")
tree = html.fromstring(response.content.decode())
self.assertEqual(len(tree.xpath("//div[@class='test_record']")), 1)
response = self.url_open(f"/model/{self.listing_controller_page.name_slugified}/fake-slug-{self.exposed_records[0].id}")
self.assertEqual(response.status_code, 404)
non_reachable_record = self.env[self.model.model].create({"name": "non_reachable"})
response = self.url_open(f"/model/{self.listing_controller_page.name_slugified}/{slug(non_reachable_record)}")
self.assertEqual(response.status_code, 404)
response = self.url_open("/model/some-other-slug")
self.assertEqual(response.status_code, 404)
self.listing_controller_page.website_published = False
response = self.url_open(f"/model/{self.listing_controller_page.name_slugified}")
self.assertEqual(response.status_code, 404)
def test_search_listing(self):
self.authenticate(None, None)
slug = self.env['ir.http']._slug
response = self.url_open(f"/model/{self.listing_controller_page.name_slugified}?search=1")
tree = html.fromstring(response.content.decode())
rec_nodes = tree.xpath("//a[@class='test_record_listing']")
self.assertEqual(len(rec_nodes), 1)
self.assertEqual(rec_nodes[0].get("href"), f"/model/{self.listing_controller_page.name_slugified}/{slug(self.exposed_records[1])}")
self.patch(ModelPageController, "pager_step", 1)
response = self.url_open(f"/model/{self.listing_controller_page.name_slugified}/page/2")
tree = html.fromstring(response.content.decode())
rec_nodes = tree.xpath("//a[@class='test_record_listing']")
self.assertEqual(len(rec_nodes), 1)
self.assertEqual(rec_nodes[0].get("href"), f"/model/{self.listing_controller_page.name_slugified}/{slug(self.exposed_records[1])}")
response = self.url_open(f"/model/{self.listing_controller_page.name_slugified}/page/2?search={self.exposed_records[0].name}")
self.assertEqual(response.url.partition('/model/')[2], f"exposed-model?search={self.exposed_records[0].name}&order=create_date+desc")
tree = html.fromstring(response.content.decode())
rec_nodes = tree.xpath("//a[@class='test_record_listing']")
self.assertEqual(len(rec_nodes), 1)
self.assertEqual(rec_nodes[0].get("href"), f"/model/{self.listing_controller_page.name_slugified}/{slug(self.exposed_records[0])}")
def test_default_layout(self):
self.assertEqual(self.listing_controller_page.default_layout, 'grid')
self.start_tour('/model/exposed-model', 'website_controller_page_listing_layout', login='admin')
self.assertEqual(self.listing_controller_page.default_layout, 'list')
#check that the user that has not previously interacted with the layout switcher will prompt on the default layout
self.start_tour('/model/exposed-model', 'website_controller_page_default_page_check', login='admin')
def test_model_constrains(self):
def get_model_meta_params(model_name):
m = self.env[model_name]
return (m._abstract, m._auto, m._transient)
default_page_vals = {
"arch": "<t><div/></t>",
"type": "qweb",
"name": "some_name"
}
# Abstract Model
self.assertEqual(get_model_meta_params("website.published.mixin"), (True, False, False))
with self.assertRaises(ValidationError) as cm:
self.env["website.controller.page"].create({**default_page_vals, "model": "website.published.mixin"})
self.assertEqual(str(cm.exception), "A page must be set to display a concrete model.")
# Transient Model
self.assertEqual(get_model_meta_params("base.partner.merge.automatic.wizard"), (False, True, True))
with self.assertRaises(ValidationError) as cm:
self.env["website.controller.page"].create({**default_page_vals, "model": "base.partner.merge.automatic.wizard"})
self.assertEqual(str(cm.exception), "A page must be set to display a concrete model.")
# _auto = False Model
self.assertEqual(get_model_meta_params("res.device"), (False, False, False))
with self.assertRaises(ValidationError) as cm:
self.env["website.controller.page"].create({**default_page_vals, "model": "res.device"})
self.assertEqual(str(cm.exception), "A page must be set to display a concrete model.")

View file

@ -0,0 +1,32 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo
import odoo.tests
@odoo.tests.tagged('-at_install', 'post_install')
class TestWebsiteFieldSanitize(odoo.tests.HttpCase):
def test_sanitize_video_iframe(self):
self.env['res.users'].create({
'name': 'Restricted Editor',
'login': 'restricted',
'password': 'restricted',
'group_ids': [(6, 0, [
self.ref('base.group_user'),
self.ref('website.group_website_restricted_editor'),
self.ref('test_website.group_test_website_admin'),
])]
})
# Add a video to an HTML field (admin).
self.start_tour(
self.env['website'].get_client_action_url('/test_website/model_item/1'),
'website_designer_iframe_video',
login='admin'
)
# Make sure a user can still edit the content (restricted editor).
self.start_tour(
self.env['website'].get_client_action_url('/test_website/model_item/1'),
'website_restricted_editor_iframe_video',
login='restricted'
)

View file

@ -0,0 +1,27 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import HttpCase, tagged
@tagged('-at_install', 'post_install')
class TestWebsitePageProperties(HttpCase):
def test_website_page_properties_common(self):
self.start_tour('/test_view', 'website_page_properties_common', login='admin')
def test_website_page_properties_can_publish(self):
self.start_tour('/test_website/model_item/1', 'website_page_properties_can_publish', login='admin')
def test_website_page_properties_website_page(self):
# Create a website page with a different URL to be tested for dependency
# tracking
self.env['website.page'].create({
'name': 'Base',
'type': 'qweb',
'arch': '<div><a href="/cool-page">Cool page</a></div>',
'key': 'test.cool_page',
'url': '/dependency_page',
'website_id': self.env['website'].search([], limit=1).id,
})
self.start_tour('/', 'website_page_properties_website_page', login='admin')

View file

@ -11,7 +11,7 @@
<t t-name='multi_url'>
<div>
<a id='get' href="/get">get</a>
<form id='post' action="/post">post</form>>
<form id='post' action="/post">post</form>
<a id='get_post' href="/get_post">get_post</a>
<a id='get_post_nomultilang' href="/get_post_nomultilang">get_post_nomultilang</a>
</div>
@ -23,29 +23,59 @@
<template id="model_item" name="Model item">
<t t-call="website.layout">
<div id="wrap">
<section t-cache="record">
<section>
<div class="container">
<div class="row">
<div class="col" t-field="record.name"/>
</div>
<div class="row">
<div class="col" t-field="record.website_description"/>
</div>
</div>
</section>
<t t-call="test_website.test_form"/>
</div>
</t>
</template>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.test.website</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="website.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='plausbile_setting']" position="after">
<div class="col-12 col-lg-6 o_setting_box" id="website_test_setting">
<button type="object" name="action_website_test_setting" string="Website test setting" class="btn-link" icon="fa-arrow-right"/>
</div>
</xpath>
</field>
</record>
<template id="test_form" name="Test Form">
<span class="hidden" data-for="test_form" t-att-data-values="{'tag_id': tag and tag.id or ''}" />
<section class="s_website_form pt16 pb16 o_colored_level" data-vcss="001" data-snippet="s_website_form" data-name="Form">
<div class="container">
<form id="test_form" action="#fake" method="post" enctype="multipart/form-data" class="o_mark_required" data-mark="*" data-pre-fill="true" data-success-mode="redirect" data-success-page="/success" data-model_name="test.model" hide-change-model="true">
<div class="s_website_form_rows row s_col_no_bgcolor">
<div class="mb-0 py-2 s_website_form_field col-12 s_website_form_required" data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="testform1">
<span class="s_website_form_label_content">Name</span>
<span class="s_website_form_mark"> *</span>
</label>
<div class="col-sm">
<input type="text" class="form-control s_website_form_input" name="name" required="1" data-fill-with="name" id="testform1"/>
</div>
</div>
</div>
<div class="mb-0 py-2 s_website_form_field col-12 s_website_form_dnone" data-name="Field"
data-type="record" data-model="test.tag">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="testmodel2">
<span class="s_website_form_label_content">Tag</span>
</label>
<div class="col-sm">
<input type="hidden" class="form-control s_website_form_input" name="tag_id" id="testmodel2" />
</div>
</div>
</div>
<div class="mb-0 py-2 col-12 s_website_form_submit" data-name="Submit Button">
<div style="width: 200px;" class="s_website_form_label"/>
<a href="#" role="button" class="btn btn-primary btn-lg s_website_form_send">Submit</a>
<span id="s_website_form_result"/>
</div>
</div>
</form>
</div>
</section>
</template>
<!-- Front end page for test model -->
<template id="test_model_page_layout" name="Test Model">

View file

@ -7,25 +7,17 @@
<field name="model">test.model.multi.website</field>
<field name="arch" type="xml">
<kanban js_class="website_pages_kanban" class="o_kanban_mobile" action="open_website_url" type="object" sample="1">
<field name="name"/>
<field name="website_url"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click d-flex flex-column">
<div class="row mb-auto">
<strong class="col-8">
<span class="o_text_overflow" t-esc="record.name.value"/>
<div class="text-muted" t-if="record.website_id.value" groups="website.group_multi_website">
<i class="fa fa-globe me-1" title="Website"/>
<field name="website_id"/>
</div>
</strong>
</div>
<div class="border-top mt-2 pt-2">
<field name="is_published" widget="boolean_toggle"/>
<t t-if="record.is_published.raw_value">Published</t>
<t t-else="">Not Published</t>
</div>
<t t-name="card">
<field name="name" class="text-truncate fw-bolder mb-auto"/>
<div class="text-muted fw-bolder" t-if="record.website_id.value" groups="website.group_multi_website">
<i class="fa fa-globe me-1" title="Website"/>
<field name="website_id"/>
</div>
<div class="d-flex border-top mt-2 pt-2">
<field name="is_published" widget="boolean_toggle"/>
<t t-if="record.is_published.raw_value">Published</t>
<t t-else="">Not Published</t>
</div>
</t>
</templates>
@ -33,47 +25,47 @@
</field>
</record>
<record id="test_model_multi_website_view_list" model="ir.ui.view">
<field name="name">Test Multi Model Pages Tree</field>
<field name="name">Test Multi Model Pages List</field>
<field name="model">test.model.multi.website</field>
<field name="priority">99</field>
<field name="arch" type="xml">
<tree js_class="website_pages_list" type="object" action="open_website_url" multi_edit="1">
<list js_class="website_pages_list" type="object" action="open_website_url" multi_edit="1">
<field name="name"/>
<field name="website_url"/>
<field name="website_id" groups="website.group_multi_website"/>
</tree>
</list>
</field>
</record>
<record id="action_test_model_multi_website" model="ir.actions.act_window">
<field name="name">Test Multi Model Pages</field>
<field name="res_model">test.model.multi.website</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('test_model_multi_website_view_list')}),
(0, 0, {'view_mode': 'list', 'view_id': ref('test_model_multi_website_view_list')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('test_model_multi_website_view_kanban')}),
]"/>
</record>
<!-- js_class bug records -->
<record id="test_model_multi_website_view_list_js_class_bug" model="ir.ui.view">
<field name="name">Test Multi Model Pages Tree js_class bug</field>
<field name="name">Test Multi Model Pages list js_class bug</field>
<field name="model">test.model.multi.website</field>
<field name="priority">99</field>
<!-- Omitting `website_pages_list` on purpose to test it does not crash -->
<field name="arch" type="xml">
<tree type="object" action="open_website_url" multi_edit="1">
<list type="object" action="open_website_url" multi_edit="1">
<field name="name"/>
<field name="website_url"/>
<field name="website_id" groups="website.group_multi_website"/>
</tree>
</list>
</field>
</record>
<record id="action_test_model_multi_website_js_class_bug" model="ir.actions.act_window">
<field name="name">Test Multi Model Pages js_class bug</field>
<field name="res_model">test.model.multi.website</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('test_model_multi_website_view_list_js_class_bug')}),
(0, 0, {'view_mode': 'list', 'view_id': ref('test_model_multi_website_view_list_js_class_bug')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('test_model_multi_website_view_kanban')}),
]"/>
</record>

View file

@ -7,21 +7,13 @@
<field name="model">test.model</field>
<field name="arch" type="xml">
<kanban js_class="website_pages_kanban" class="o_kanban_mobile" action="open_website_url" type="object" sample="1">
<field name="name"/>
<field name="website_url"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click d-flex flex-column">
<div class="row mb-auto">
<strong class="col-8">
<span class="o_text_overflow" t-esc="record.name.value"/>
</strong>
</div>
<div class="border-top mt-2 pt-2">
<field name="is_published" widget="boolean_toggle"/>
<t t-if="record.is_published.raw_value">Published</t>
<t t-else="">Not Published</t>
</div>
<t t-name="card">
<field class="text-truncate mb-auto fw-bolder" name="name"/>
<div class="d-flex border-top mt-2 pt-2">
<field name="is_published" widget="boolean_toggle"/>
<t t-if="record.is_published.raw_value">Published</t>
<t t-else="">Not Published</t>
</div>
</t>
</templates>
@ -29,22 +21,22 @@
</field>
</record>
<record id="test_model_view_list" model="ir.ui.view">
<field name="name">Test Model Pages Tree</field>
<field name="name">Test Model Pages List</field>
<field name="model">test.model</field>
<field name="priority">99</field>
<field name="arch" type="xml">
<tree js_class="website_pages_list" type="object" action="open_website_url" multi_edit="1">
<list js_class="website_pages_list" type="object" action="open_website_url" multi_edit="1">
<field name="name"/>
<field name="website_url"/>
</tree>
</list>
</field>
</record>
<record id="action_test_model" model="ir.actions.act_window">
<field name="name">Test Model Pages</field>
<field name="res_model">test.model</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('test_model_view_list')}),
(0, 0, {'view_mode': 'list', 'view_id': ref('test_model_view_list')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('test_model_view_kanban')}),
]"/>
</record>
@ -54,6 +46,7 @@
<field name="name">Test Model</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">test.model</field>
<field name="path">test-model</field>
<field name="view_id" eval="False"/>
</record>