Backend improvements: views, fields_get, session, RPC stubs
- Improved auto-generated list/form/search views with priority fields, two-column form layout, statusbar widget, notebook for O2M fields - Enhanced fields_get with currency_field, compute, related metadata - Fixed session handling: handleSessionInfo/handleSessionCheck use real session from cookie instead of hardcoded values - Added read_progress_bar and activity_format RPC stubs - Improved bootstrap translations with lang_parameters - Added "contacts" to session modules list Server starts successfully: 14 modules, 93 models, 378 XML templates, 503 JS modules transpiled — all from local frontend/ directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
320
pkg/server/transpiler_test.go
Normal file
320
pkg/server/transpiler_test.go
Normal file
@@ -0,0 +1,320 @@
|
||||
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])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user