- Portal: /my/* routes, signup, password reset, portal user support - Email Inbound: IMAP polling (go-imap/v2), thread matching - Discuss: mail.channel, long-polling bus, DM, unread count - Cron: ir.cron runner (goroutine scheduler) - Bank Import, CSV/Excel Import - Automation (ir.actions.server) - Fetchmail service - HR Payroll model - Various fixes across account, sale, stock, purchase, crm, hr, project Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
324 lines
9.0 KiB
Go
324 lines
9.0 KiB
Go
package server
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestURLToModuleName(t *testing.T) {
|
|
tests := []struct {
|
|
url string
|
|
want string
|
|
}{
|
|
{"/web/static/src/core/foo.js", "@web/core/foo"},
|
|
{"/web/static/src/env.js", "@web/env"},
|
|
{"/web/static/src/session.js", "@web/session"},
|
|
{"/stock/static/src/widgets/foo.js", "@stock/widgets/foo"},
|
|
{"/web/static/lib/owl/owl.js", "@web/../lib/owl/owl"},
|
|
{"/web/static/src/core/browser/browser.js", "@web/core/browser/browser"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.url, func(t *testing.T) {
|
|
got := URLToModuleName(tt.url)
|
|
if got != tt.want {
|
|
t.Errorf("URLToModuleName(%q) = %q, want %q", tt.url, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsOdooModule(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
content string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "has import",
|
|
url: "/web/static/src/foo.js",
|
|
content: `import { Foo } from "@web/bar";`,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "has export",
|
|
url: "/web/static/src/foo.js",
|
|
content: `export class Foo {}`,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "has odoo-module tag",
|
|
url: "/web/static/src/foo.js",
|
|
content: "// @odoo-module\nconst x = 1;",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "ignore directive",
|
|
url: "/web/static/src/foo.js",
|
|
content: "// @odoo-module ignore\nimport { X } from '@web/foo';",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "plain JS no module",
|
|
url: "/web/static/src/foo.js",
|
|
content: "var x = 1;\nconsole.log(x);",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "not a JS file",
|
|
url: "/web/static/src/foo.xml",
|
|
content: `import { Foo } from "@web/bar";`,
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := IsOdooModule(tt.url, tt.content)
|
|
if got != tt.want {
|
|
t.Errorf("IsOdooModule(%q, ...) = %v, want %v", tt.url, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractImports(t *testing.T) {
|
|
t.Run("named imports", func(t *testing.T) {
|
|
content := `import { Foo, Bar } from "@web/core/foo";
|
|
import { Baz as Qux } from "@web/core/baz";
|
|
const x = 1;`
|
|
deps, requires, clean := extractImports("test.module", content)
|
|
|
|
if len(deps) != 2 {
|
|
t.Fatalf("expected 2 deps, got %d: %v", len(deps), deps)
|
|
}
|
|
if deps[0] != "@web/core/foo" {
|
|
t.Errorf("deps[0] = %q, want @web/core/foo", deps[0])
|
|
}
|
|
if deps[1] != "@web/core/baz" {
|
|
t.Errorf("deps[1] = %q, want @web/core/baz", deps[1])
|
|
}
|
|
|
|
if len(requires) != 2 {
|
|
t.Fatalf("expected 2 requires, got %d", len(requires))
|
|
}
|
|
if !strings.Contains(requires[0], `{ Foo, Bar }`) {
|
|
t.Errorf("requires[0] = %q, want Foo, Bar destructuring", requires[0])
|
|
}
|
|
if !strings.Contains(requires[1], `Baz: Qux`) {
|
|
t.Errorf("requires[1] = %q, want Baz: Qux alias", requires[1])
|
|
}
|
|
|
|
if strings.Contains(clean, "import") {
|
|
t.Errorf("clean content still contains import statements: %s", clean)
|
|
}
|
|
if !strings.Contains(clean, "const x = 1;") {
|
|
t.Errorf("clean content should still have 'const x = 1;': %s", clean)
|
|
}
|
|
})
|
|
|
|
t.Run("default import", func(t *testing.T) {
|
|
content := `import Foo from "@web/core/foo";`
|
|
deps, requires, _ := extractImports("test.module", content)
|
|
|
|
if len(deps) != 1 || deps[0] != "@web/core/foo" {
|
|
t.Errorf("deps = %v, want [@web/core/foo]", deps)
|
|
}
|
|
if len(requires) != 1 || !strings.Contains(requires[0], `Symbol.for("default")`) {
|
|
t.Errorf("requires = %v, want default symbol access", requires)
|
|
}
|
|
})
|
|
|
|
t.Run("namespace import", func(t *testing.T) {
|
|
content := `import * as utils from "@web/core/utils";`
|
|
deps, requires, _ := extractImports("test.module", content)
|
|
|
|
if len(deps) != 1 || deps[0] != "@web/core/utils" {
|
|
t.Errorf("deps = %v, want [@web/core/utils]", deps)
|
|
}
|
|
if len(requires) != 1 || !strings.Contains(requires[0], `const utils = require("@web/core/utils")`) {
|
|
t.Errorf("requires = %v, want namespace require", requires)
|
|
}
|
|
})
|
|
|
|
t.Run("side-effect import", func(t *testing.T) {
|
|
content := `import "@web/core/setup";`
|
|
deps, requires, _ := extractImports("test.module", content)
|
|
|
|
if len(deps) != 1 || deps[0] != "@web/core/setup" {
|
|
t.Errorf("deps = %v, want [@web/core/setup]", deps)
|
|
}
|
|
if len(requires) != 1 || requires[0] != `require("@web/core/setup");` {
|
|
t.Errorf("requires = %v, want side-effect require", requires)
|
|
}
|
|
})
|
|
|
|
t.Run("dedup deps", func(t *testing.T) {
|
|
content := `import { Foo } from "@web/core/foo";
|
|
import { Bar } from "@web/core/foo";`
|
|
deps, _, _ := extractImports("test.module", content)
|
|
|
|
if len(deps) != 1 {
|
|
t.Errorf("expected deduped deps, got %v", deps)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestTransformExports(t *testing.T) {
|
|
t.Run("export class", func(t *testing.T) {
|
|
got, _ := transformExports("export class Foo extends Bar {")
|
|
want := "const Foo = __exports.Foo = class Foo extends Bar {"
|
|
if got != want {
|
|
t.Errorf("got %q, want %q", got, want)
|
|
}
|
|
})
|
|
|
|
t.Run("export function", func(t *testing.T) {
|
|
got, deferred := transformExports("export function doSomething(a, b) {")
|
|
want := `function doSomething(a, b) {`
|
|
if got != want {
|
|
t.Errorf("got %q, want %q", got, want)
|
|
}
|
|
if len(deferred) != 1 || deferred[0] != "doSomething" {
|
|
t.Errorf("deferred = %v, want [doSomething]", deferred)
|
|
}
|
|
})
|
|
|
|
t.Run("export const", func(t *testing.T) {
|
|
got, _ := transformExports("export const MAX_SIZE = 100;")
|
|
want := "const MAX_SIZE = __exports.MAX_SIZE = 100;"
|
|
if got != want {
|
|
t.Errorf("got %q, want %q", got, want)
|
|
}
|
|
})
|
|
|
|
t.Run("export let", func(t *testing.T) {
|
|
got, _ := transformExports("export let counter = 0;")
|
|
want := "let counter = __exports.counter = 0;"
|
|
if got != want {
|
|
t.Errorf("got %q, want %q", got, want)
|
|
}
|
|
})
|
|
|
|
t.Run("export default", func(t *testing.T) {
|
|
got, _ := transformExports("export default Foo;")
|
|
want := `__exports[Symbol.for("default")] = Foo;`
|
|
if got != want {
|
|
t.Errorf("got %q, want %q", got, want)
|
|
}
|
|
})
|
|
|
|
t.Run("export named", func(t *testing.T) {
|
|
got, _ := transformExports("export { Foo, Bar };")
|
|
if !strings.Contains(got, "__exports.Foo = Foo;") {
|
|
t.Errorf("missing Foo export in: %s", got)
|
|
}
|
|
if !strings.Contains(got, "__exports.Bar = Bar;") {
|
|
t.Errorf("missing Bar export in: %s", got)
|
|
}
|
|
})
|
|
|
|
t.Run("export named with alias", func(t *testing.T) {
|
|
got, _ := transformExports("export { Foo as default };")
|
|
if !strings.Contains(got, "__exports.default = Foo;") {
|
|
t.Errorf("missing aliased export in: %s", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestTranspileJS(t *testing.T) {
|
|
t.Run("full transpile", func(t *testing.T) {
|
|
content := `// @odoo-module
|
|
import { Component } from "@odoo/owl";
|
|
import { registry } from "@web/core/registry";
|
|
|
|
export class MyWidget extends Component {
|
|
static template = "web.MyWidget";
|
|
}
|
|
|
|
registry.category("actions").add("my_widget", MyWidget);
|
|
`
|
|
url := "/web/static/src/views/my_widget.js"
|
|
result := TranspileJS(url, content)
|
|
|
|
// Check wrapper
|
|
if !strings.HasPrefix(result, `odoo.define("@web/views/my_widget"`) {
|
|
t.Errorf("missing odoo.define header: %s", result[:80])
|
|
}
|
|
|
|
// Check deps
|
|
if !strings.Contains(result, `"@odoo/owl"`) {
|
|
t.Errorf("missing @odoo/owl dependency")
|
|
}
|
|
if !strings.Contains(result, `"@web/core/registry"`) {
|
|
t.Errorf("missing @web/core/registry dependency")
|
|
}
|
|
|
|
// Check require lines
|
|
if !strings.Contains(result, `const { Component } = require("@odoo/owl");`) {
|
|
t.Errorf("missing Component require")
|
|
}
|
|
if !strings.Contains(result, `const { registry } = require("@web/core/registry");`) {
|
|
t.Errorf("missing registry require")
|
|
}
|
|
|
|
// Check export transform
|
|
if !strings.Contains(result, `const MyWidget = __exports.MyWidget = class MyWidget`) {
|
|
t.Errorf("missing class export transform")
|
|
}
|
|
|
|
// Check no raw import/export left
|
|
if strings.Contains(result, "import {") {
|
|
t.Errorf("raw import statement still present")
|
|
}
|
|
|
|
// Check wrapper close
|
|
if !strings.Contains(result, "return __exports;") {
|
|
t.Errorf("missing return __exports")
|
|
}
|
|
})
|
|
|
|
t.Run("non-module passthrough", func(t *testing.T) {
|
|
content := "var x = 1;\nconsole.log(x);"
|
|
result := TranspileJS("/web/static/lib/foo.js", content)
|
|
if result != content {
|
|
t.Errorf("non-module content was modified")
|
|
}
|
|
})
|
|
|
|
t.Run("ignore directive passthrough", func(t *testing.T) {
|
|
content := "// @odoo-module ignore\nimport { X } from '@web/foo';\nexport class Y {}"
|
|
result := TranspileJS("/web/static/src/foo.js", content)
|
|
if result != content {
|
|
t.Errorf("ignored module content was modified")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestParseImportSpecifiers(t *testing.T) {
|
|
tests := []struct {
|
|
raw string
|
|
want []importSpecifier
|
|
}{
|
|
{"Foo, Bar", []importSpecifier{{name: "Foo"}, {name: "Bar"}}},
|
|
{"Foo as F, Bar", []importSpecifier{{name: "Foo", alias: "F"}, {name: "Bar"}}},
|
|
{" X , Y , Z ", []importSpecifier{{name: "X"}, {name: "Y"}, {name: "Z"}}},
|
|
{"", nil},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.raw, func(t *testing.T) {
|
|
got := parseImportSpecifiers(tt.raw)
|
|
if len(got) != len(tt.want) {
|
|
t.Fatalf("got %d specifiers, want %d", len(got), len(tt.want))
|
|
}
|
|
for i, s := range got {
|
|
if s.name != tt.want[i].name || s.alias != tt.want[i].alias {
|
|
t.Errorf("specifier[%d] = %+v, want %+v", i, s, tt.want[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|