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(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(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(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(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(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 := transformExports("export function doSomething(a, b) {") want := `__exports.doSomething = function doSomething(a, b) {` if got != want { t.Errorf("got %q, want %q", got, want) } }) 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]) } } }) } }