summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/interactive/background-repeat.js28
-rw-r--r--tests/interactive/background-size.js115
-rw-r--r--tests/interactive/border-radius.js61
-rw-r--r--tests/interactive/border-width.js58
-rw-r--r--tests/interactive/borders.js133
-rw-r--r--tests/interactive/box-layout.js85
-rw-r--r--tests/interactive/box-shadow-animated.js80
-rw-r--r--tests/interactive/box-shadows.js56
-rw-r--r--tests/interactive/calendar.js28
-rw-r--r--tests/interactive/css-fonts.js40
-rw-r--r--tests/interactive/entry.js57
-rwxr-xr-xtests/interactive/gapplication.js104
-rw-r--r--tests/interactive/icons.js79
-rw-r--r--tests/interactive/inline-style.js46
-rw-r--r--tests/interactive/scroll-view-sizing.js395
-rw-r--r--tests/interactive/scrolling.js51
-rwxr-xr-xtests/interactive/test-title.js37
-rw-r--r--tests/interactive/transitions.js35
-rw-r--r--tests/meson.build18
-rwxr-xr-xtests/run-test.sh.in45
-rw-r--r--tests/testcommon/100-200.svg21
-rw-r--r--tests/testcommon/200-100.svg21
-rw-r--r--tests/testcommon/200-200.svg21
-rw-r--r--tests/testcommon/border-image.pngbin0 -> 981 bytes
-rw-r--r--tests/testcommon/face-plain.pngbin0 -> 4298 bytes
-rw-r--r--tests/testcommon/test.css112
-rw-r--r--tests/testcommon/ui.js28
-rw-r--r--tests/unit/insertSorted.js76
-rw-r--r--tests/unit/jsParse.js194
-rw-r--r--tests/unit/markup.js143
-rw-r--r--tests/unit/params.js32
-rw-r--r--tests/unit/url.js77
32 files changed, 2276 insertions, 0 deletions
diff --git a/tests/interactive/background-repeat.js b/tests/interactive/background-repeat.js
new file mode 100644
index 0000000..1377f74
--- /dev/null
+++ b/tests/interactive/background-repeat.js
@@ -0,0 +1,28 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const UI = imports.testcommon.ui;
+
+const { Clutter, St } = imports.gi;
+
+function test() {
+ let stage = new Clutter.Stage({ width: 640, height: 480 });
+ UI.init(stage);
+
+ let vbox = new St.BoxLayout({ width: stage.width,
+ height: stage.height,
+ style: 'background: #ffee88;' });
+ stage.add_actor(vbox);
+
+ let scroll = new St.ScrollView();
+ vbox.add(scroll, { expand: true });
+
+ let box = new St.BoxLayout({ vertical: true });
+ scroll.add_actor(box);
+
+ let contents = new St.Widget({ width: 1000, height: 1000,
+ style_class: 'background-image background-repeat' });
+ box.add_actor(contents);
+
+ UI.main(stage);
+}
+test();
diff --git a/tests/interactive/background-size.js b/tests/interactive/background-size.js
new file mode 100644
index 0000000..064bc9e
--- /dev/null
+++ b/tests/interactive/background-size.js
@@ -0,0 +1,115 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const UI = imports.testcommon.ui;
+
+const { Cogl, Clutter, Meta, St } = imports.gi;
+
+
+function test() {
+ Meta.init();
+
+ let stage = Meta.get_backend().get_stage();
+ UI.init(stage);
+
+ let vbox = new St.BoxLayout({ style: 'background: #ffee88;' });
+ vbox.add_constraint(new Clutter.BindConstraint({ source: stage,
+ coordinate: Clutter.BindCoordinate.SIZE }));
+ stage.add_actor(vbox);
+
+ let scroll = new St.ScrollView();
+ vbox.add(scroll, { expand: true });
+
+ vbox = new St.BoxLayout({ vertical: true,
+ style: 'padding: 10px;'
+ + 'spacing: 20px;' });
+ scroll.add_actor(vbox);
+
+ let tbox = null;
+
+ function addTestCase(image, size, backgroundSize, useCairo) {
+ // Using a border in CSS forces cairo rendering.
+ // To get a border using cogl, we paint a border using
+ // paint signal hacks.
+
+ let obin = new St.Bin();
+ if (useCairo)
+ obin.style = 'border: 3px solid green;';
+ else
+ obin.connect_after('paint', (actor, paintContext) => {
+ let framebuffer = paintContext.get_framebuffer();
+ let coglContext = framebuffer.get_context();
+
+ let pipeline = new Cogl.Pipeline(coglContext);
+ pipeline.set_color4f(0, 1, 0, 1);
+
+ let alloc = actor.get_allocation_box();
+ let width = 3;
+
+ // clockwise order
+ framebuffer.draw_rectangle(pipeline,
+ 0, 0, alloc.get_width(), width);
+ framebuffer.draw_rectangle(pipeline,
+ alloc.get_width() - width, width,
+ alloc.get_width(), alloc.get_height());
+ framebuffer.draw_rectangle(pipeline,
+ 0,
+ alloc.get_height(),
+ alloc.get_width() - width,
+ alloc.get_height() - width);
+ framebuffer.draw_rectangle(pipeline,
+ 0,
+ alloc.get_height() - width,
+ width,
+ width);
+ });
+ tbox.add(obin);
+
+ let [width, height] = size;
+ let bin = new St.Bin({ style_class: 'background-image-' + image,
+ width: width,
+ height: height,
+ style: 'border: 1px solid transparent;'
+ + 'background-size: ' + backgroundSize + ';',
+ x_fill: true,
+ y_fill: true
+ });
+ obin.set_child(bin);
+
+ bin.set_child(new St.Label({ text: backgroundSize + (useCairo ? ' (cairo)' : ' (cogl)'),
+ style: 'font-size: 15px;'
+ + 'text-align: center;'
+ }));
+ }
+
+ function addTestLine(image, size, useCairo) {
+ const backgroundSizes = ["auto", "contain", "cover", "200px 200px", "100px 100px", "100px 200px"];
+
+ let [width, height] = size;
+ vbox.add(new St.Label({ text: image + '.svg / ' + width + '×' + height,
+ style: 'font-size: 15px;'
+ + 'text-align: center;'
+ }));
+
+ tbox = new St.BoxLayout({ style: 'spacing: 20px;' });
+ vbox.add(tbox);
+
+ for (let s of backgroundSizes)
+ addTestCase(image, size, s, false);
+ for (let s of backgroundSizes)
+ addTestCase(image, size, s, true);
+ }
+
+ function addTestImage(image) {
+ const containerSizes = [[100, 100], [200, 200], [250, 250], [100, 250], [250, 100]];
+
+ for (let size of containerSizes)
+ addTestLine(image, size);
+ }
+
+ addTestImage ('200-200');
+ addTestImage ('200-100');
+ addTestImage ('100-200');
+
+ UI.main(stage);
+}
+test();
diff --git a/tests/interactive/border-radius.js b/tests/interactive/border-radius.js
new file mode 100644
index 0000000..4d26518
--- /dev/null
+++ b/tests/interactive/border-radius.js
@@ -0,0 +1,61 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const UI = imports.testcommon.ui;
+
+const { Clutter, St } = imports.gi;
+
+function test() {
+ let stage = new Clutter.Stage({ width: 640, height: 480 });
+ UI.init(stage);
+
+ let vbox = new St.BoxLayout({ width: stage.width,
+ height: stage.height,
+ style: 'background: #ffee88;' });
+ stage.add_actor(vbox);
+
+ let scroll = new St.ScrollView();
+ vbox.add(scroll, { expand: true });
+
+ let box = new St.BoxLayout({ vertical: true,
+ style: 'padding: 10px;'
+ + 'spacing: 20px;' });
+ scroll.add_actor(box);
+
+ function addTestCase(radii, useGradient) {
+ let background;
+ if (useGradient)
+ background = 'background-gradient-direction: vertical;'
+ + 'background-gradient-start: white;'
+ + 'background-gradient-end: gray;';
+ else
+ background = 'background: white;';
+
+ box.add(new St.Label({ text: "border-radius: " + radii + ";",
+ style: 'border: 1px solid black; '
+ + 'border-radius: ' + radii + ';'
+ + 'padding: 5px;' + background }),
+ { x_fill: false });
+ }
+
+ // uniform backgrounds
+ addTestCase(" 0px 5px 10px 15px", false);
+ addTestCase(" 5px 10px 15px 0px", false);
+ addTestCase("10px 15px 0px 5px", false);
+ addTestCase("15px 0px 5px 10px", false);
+
+ // gradient backgrounds
+ addTestCase(" 0px 5px 10px 15px", true);
+ addTestCase(" 5px 10px 15px 0px", true);
+ addTestCase("10px 15px 0px 5px", true);
+ addTestCase("15px 0px 5px 10px", true);
+
+ // border-radius reduction
+ // these should all take the cairo fallback,
+ // so don't bother testing w/ or w/out gradients.
+ addTestCase("200px 200px 200px 200px", false);
+ addTestCase("200px 200px 0px 200px", false);
+ addTestCase("999px 0px 999px 0px", false);
+
+ UI.main(stage);
+}
+test();
diff --git a/tests/interactive/border-width.js b/tests/interactive/border-width.js
new file mode 100644
index 0000000..30c7575
--- /dev/null
+++ b/tests/interactive/border-width.js
@@ -0,0 +1,58 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const UI = imports.testcommon.ui;
+
+const { Clutter, St } = imports.gi;
+
+function test() {
+ let stage = new Clutter.Stage({ width: 640, height: 480 });
+ UI.init(stage);
+
+ let vbox = new St.BoxLayout({ width: stage.width,
+ height: stage.height,
+ style: 'padding: 10px; background: #ffee88;'
+ });
+ stage.add_actor(vbox);
+
+ let scroll = new St.ScrollView();
+ vbox.add(scroll, { expand: true });
+
+ let box = new St.BoxLayout({ vertical: true,
+ style: 'spacing: 20px;' });
+ scroll.add_actor(box);
+
+ function addTestCase(borders, useGradient) {
+ let background;
+ if (useGradient)
+ background = 'background-gradient-direction: vertical;'
+ + 'background-gradient-start: white;'
+ + 'background-gradient-end: gray;';
+ else
+ background = 'background: white;';
+
+ let border_style = "border-top: " + borders[St.Side.TOP] + " solid black;\n" +
+ "border-right: " + borders[St.Side.RIGHT] + " solid black;\n" +
+ "border-bottom: " + borders[St.Side.BOTTOM] + " solid black;\n" +
+ "border-left: " + borders[St.Side.LEFT] + " solid black;";
+ box.add(new St.Label({ text: border_style,
+ style: border_style
+ + 'border-radius: 0px 5px 15px 25px;'
+ + 'padding: 5px;' + background }),
+ { x_fill: false });
+ }
+
+ // uniform backgrounds
+ addTestCase([" 0px", " 5px", "10px", "15px"], false);
+ addTestCase([" 5px", "10px", "15px", " 0px"], false);
+ addTestCase(["10px", "15px", " 0px", " 5px"], false);
+ addTestCase(["15px", " 0px", " 5px", "10px"], false);
+
+ // gradient backgrounds
+ addTestCase([" 0px", " 5px", "10px", "15px"], true);
+ addTestCase([" 5px", "10px", "15px", " 0px"], true);
+ addTestCase(["10px", "15px", " 0px", " 5px"], true);
+ addTestCase(["15px", " 0px", " 5px", "10px"], true);
+
+ UI.main(stage);
+}
+test();
diff --git a/tests/interactive/borders.js b/tests/interactive/borders.js
new file mode 100644
index 0000000..4812acb
--- /dev/null
+++ b/tests/interactive/borders.js
@@ -0,0 +1,133 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const UI = imports.testcommon.ui;
+
+const { Clutter, St } = imports.gi;
+
+function test() {
+ let stage = new Clutter.Stage({ width: 640, height: 480 });
+ UI.init(stage);
+
+ let vbox = new St.BoxLayout({ width: stage.width,
+ height: stage.height,
+ style: 'background: #ffee88;' });
+ stage.add_actor(vbox);
+
+ let scroll = new St.ScrollView();
+ vbox.add(scroll, { expand: true });
+
+ let box = new St.BoxLayout({ vertical: true,
+ style: 'padding: 10px;'
+ + 'spacing: 20px;' });
+ scroll.add_actor(box);
+
+ box.add(new St.Label({ text: "Hello World",
+ style: 'border: 1px solid black; '
+ + 'padding: 5px;' }));
+
+ box.add(new St.Label({ text: "Hello Round World",
+ style: 'border: 3px solid green; '
+ + 'border-radius: 8px; '
+ + 'padding: 5px;' }));
+
+ box.add(new St.Label({ text: "Hello Background",
+ style: 'border: 3px solid green; '
+ + 'border-radius: 8px; '
+ + 'background: white; '
+ + 'padding: 5px;' }));
+
+ box.add(new St.Label({ text: "Hello Translucent Black Border",
+ style: 'border: 3px solid rgba(0, 0, 0, 0.4); '
+ + 'background: white; ' }));
+
+ box.add(new St.Label({ text: "Hello Translucent Background",
+ style: 'background: rgba(255, 255, 255, 0.3);' }));
+
+ box.add(new St.Label({ text: "Border, Padding, Content: 20px" }));
+
+ let b1 = new St.BoxLayout({ vertical: true,
+ style: 'border: 20px solid black; '
+ + 'background: white; '
+ + 'padding: 20px;' });
+ box.add(b1);
+
+ b1.add(new St.BoxLayout({ width: 20, height: 20,
+ style: 'background: black' }));
+
+ box.add(new St.Label({ text: "Translucent big blue border, with rounding",
+ style: 'border: 20px solid rgba(0, 0, 255, 0.2); '
+ + 'border-radius: 10px; '
+ + 'background: white; '
+ + 'padding: 10px;' }));
+
+ box.add(new St.Label({ text: "Transparent border",
+ style: 'border: 20px solid transparent; '
+ + 'background: white; '
+ + 'padding: 10px;' }));
+
+ box.add(new St.Label({ text: "Border Image",
+ style_class: "border-image",
+ style: "padding: 10px;" }));
+
+ box.add(new St.Label({ text: "Border Image with Gradient",
+ style_class: 'border-image-with-background-gradient',
+ style: "padding: 10px;"
+ + 'background-gradient-direction: vertical;' }));
+
+ box.add(new St.Label({ text: "Rounded, framed, shadowed gradients" }));
+
+ let framedGradients = new St.BoxLayout({ vertical: false,
+ style: 'padding: 10px; spacing: 12px;' });
+ box.add(framedGradients);
+
+ function addGradientCase(direction, borderWidth, borderRadius, extra) {
+ let gradientBox = new St.BoxLayout({ style_class: 'background-gradient',
+ style: 'border: ' + borderWidth + 'px solid #8b0000;'
+ + 'border-radius: ' + borderRadius + 'px;'
+ + 'background-gradient-direction: ' + direction + ';'
+ + 'width: 32px;'
+ + 'height: 32px;'
+ + extra });
+ framedGradients.add(gradientBox, { x_fill: false, y_fill: false } );
+ }
+
+ addGradientCase ('horizontal', 0, 5, 'box-shadow: 0px 0px 0px 0px rgba(0,0,0,0.5);');
+ addGradientCase ('horizontal', 2, 5, 'box-shadow: 0px 2px 0px 0px rgba(0,255,0,0.5);');
+ addGradientCase ('horizontal', 5, 2, 'box-shadow: 2px 0px 0px 0px rgba(0,0,255,0.5);');
+ addGradientCase ('horizontal', 5, 20, 'box-shadow: 0px 0px 4px 0px rgba(255,0,0,0.5);');
+ addGradientCase ('vertical', 0, 5, 'box-shadow: 0px 0px 0px 4px rgba(0,0,0,0.5);');
+ addGradientCase ('vertical', 2, 5, 'box-shadow: 0px 0px 4px 4px rgba(0,0,0,0.5);');
+ addGradientCase ('vertical', 5, 2, 'box-shadow: -2px -2px 6px 0px rgba(0,0,0,0.5);');
+ addGradientCase ('vertical', 5, 20, 'box-shadow: -2px -2px 0px 6px rgba(0,0,0,0.5);');
+
+ box.add(new St.Label({ text: "Rounded, framed, shadowed images" }));
+
+ let framedImages = new St.BoxLayout({ vertical: false,
+ style: 'padding: 10px; spacing: 6px;' });
+ box.add(framedImages);
+
+ function addBackgroundImageCase(borderWidth, borderRadius, width, height, extra) {
+ let imageBox = new St.BoxLayout({ style_class: 'background-image',
+ style: 'border: ' + borderWidth + 'px solid #8b8b8b;'
+ + 'border-radius: ' + borderRadius + 'px;'
+ + 'width: ' + width + 'px;'
+ + 'height: ' + height + 'px;'
+ + extra });
+ framedImages.add(imageBox, { x_fill: false, y_fill: false } );
+ }
+
+ addBackgroundImageCase (0, 0, 32, 32, 'background-position: 2px 5px');
+ addBackgroundImageCase (0, 0, 16, 16, '-st-background-image-shadow: 1px 1px 4px 0px rgba(0,0,0,0.5); background-color: rgba(0,0,0,0)');
+ addBackgroundImageCase (0, 5, 32, 32, '-st-background-image-shadow: 0px 0px 0px 0px rgba(0,0,0,0.5);');
+ addBackgroundImageCase (2, 5, 32, 32, '-st-background-image-shadow: 0px 2px 0px 0px rgba(0,255,0,0.5);');
+ addBackgroundImageCase (5, 2, 32, 32, '-st-background-image-shadow: 2px 0px 0px 0px rgba(0,0,255,0.5);');
+ addBackgroundImageCase (5, 20, 32, 32, '-st-background-image-shadow: 0px 0px 4px 0px rgba(255,0,0,0.5);');
+ addBackgroundImageCase (0, 5, 48, 48, '-st-background-image-shadow: 0px 0px 0px 4px rgba(0,0,0,0.5);');
+ addBackgroundImageCase (5, 5, 48, 48, '-st-background-image-shadow: 0px 0px 4px 4px rgba(0,0,0,0.5);');
+ addBackgroundImageCase (0, 5, 64, 64, '-st-background-image-shadow: -2px -2px 6px 0px rgba(0,0,0,0.5);');
+ addBackgroundImageCase (5, 5, 64, 64, '-st-background-image-shadow: -2px -2px 0px 6px rgba(0,0,0,0.5);');
+ addBackgroundImageCase (0, 5, 32, 32, 'background-position: 2px 5px');
+
+ UI.main(stage);
+}
+test();
diff --git a/tests/interactive/box-layout.js b/tests/interactive/box-layout.js
new file mode 100644
index 0000000..bb9a5bb
--- /dev/null
+++ b/tests/interactive/box-layout.js
@@ -0,0 +1,85 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const UI = imports.testcommon.ui;
+
+const { Clutter, St } = imports.gi;
+
+function test() {
+ let stage = new Clutter.Stage();
+ UI.init(stage);
+
+ let vbox = new St.BoxLayout({ vertical: true,
+ width: stage.width,
+ height: stage.height,
+ style: 'padding: 10px;'
+ + 'spacing: 10px;' });
+ stage.add_actor(vbox);
+
+ ////////////////////////////////////////////////////////////////////////////////
+
+ let colored_boxes = new St.BoxLayout({ vertical: true,
+ width: 200,
+ height: 200,
+ style: 'border: 2px solid black;' });
+ vbox.add(colored_boxes, { x_fill: false,
+ x_align: St.Align.MIDDLE });
+
+ let b2 = new St.BoxLayout({ style: 'border: 2px solid #666666' });
+ colored_boxes.add(b2, { expand: true });
+
+ b2.add(new St.Label({ text: "Expand",
+ style: 'border: 1px solid #aaaaaa; '
+ + 'background: #ffeecc' }),
+ { expand: true });
+ b2.add(new St.Label({ text: "Expand\nNo Fill",
+ style: 'border: 1px solid #aaaaaa; '
+ + 'background: #ccffaa' }),
+ { expand: true,
+ x_fill: false,
+ x_align: St.Align.MIDDLE,
+ y_fill: false,
+ y_align: St.Align.MIDDLE });
+
+ colored_boxes.add(new St.Label({ text: "Default",
+ style: 'border: 1px solid #aaaaaa; '
+ + 'background: #cceeff' }));
+
+ ////////////////////////////////////////////////////////////////////////////////
+
+ function createCollapsableBox(width) {
+ let b = new St.BoxLayout({ width: width,
+ style: 'border: 1px solid black;'
+ + 'font: 13px Sans;' });
+ b.add(new St.Label({ text: "Very Very Very Long",
+ style: 'background: #ffaacc;'
+ + 'padding: 5px; '
+ + 'border: 1px solid #666666;' }),
+ { expand: true });
+ b.add(new St.Label({ text: "Very Very Long",
+ style: 'background: #ffeecc; '
+ + 'padding: 5px; '
+ + 'border: 1px solid #666666;' }),
+ { expand: true });
+ b.add(new St.Label({ text: "Very Long",
+ style: 'background: #ccffaa; '
+ + 'padding: 5px; '
+ + 'border: 1px solid #666666;' }),
+ { expand: true });
+ b.add(new St.Label({ text: "Short",
+ style: 'background: #cceeff; '
+ + 'padding: 5px; '
+ + 'border: 1px solid #666666;' }),
+ { expand: true });
+
+ return b;
+ }
+
+ for (let width = 200; width <= 500; width += 60 ) {
+ vbox.add(createCollapsableBox (width),
+ { x_fill: false,
+ x_align: St.Align.MIDDLE });
+ }
+
+ UI.main(stage);
+}
+test();
diff --git a/tests/interactive/box-shadow-animated.js b/tests/interactive/box-shadow-animated.js
new file mode 100644
index 0000000..cf117a7
--- /dev/null
+++ b/tests/interactive/box-shadow-animated.js
@@ -0,0 +1,80 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const UI = imports.testcommon.ui;
+
+const { Clutter, GLib, St } = imports.gi;
+
+const DELAY = 2000;
+
+function resize_animated(label) {
+ if (label.width == 100) {
+ label.save_easing_state();
+ label.set_easing_mode(Clutter.AnimationMode.EASE_OUT_QUAD);
+ label.set_easing_duration(DELAY - 50);
+ label.set_size(500, 500);
+ label.restore_easing_state();
+ } else {
+ label.save_easing_state();
+ label.set_easing_mode(Clutter.AnimationMode.EASE_OUT_QUAD);
+ label.set_easing_duration(DELAY - 50);
+ label.set_size(100, 100);
+ label.restore_easing_state();
+ }
+}
+
+function get_css_style(shadow_style)
+{
+ return 'border: 20px solid black;' +
+ 'border-radius: 20px;' +
+ 'background-color: white; ' +
+ 'padding: 5px;' + shadow_style;
+}
+
+function test() {
+ let stage = new Clutter.Stage({ width: 1000, height: 600 });
+ UI.init(stage);
+
+ let iter = 0;
+ let shadowStyles = [ 'box-shadow: 3px 50px 0px 4px rgba(0,0,0,0.5);',
+ 'box-shadow: 3px 4px 10px 4px rgba(0,0,0,0.5);',
+ 'box-shadow: 0px 50px 0px 0px rgba(0,0,0,0.5);',
+ 'box-shadow: 100px 100px 20px 4px rgba(0,0,0,0.5);'];
+ let label1 = new St.Label({ style: get_css_style(shadowStyles[iter]),
+ text: shadowStyles[iter],
+ x: 20,
+ y: 20,
+ width: 100,
+ height: 100
+ });
+ stage.add_actor(label1);
+ let label2 = new St.Label({ style: get_css_style(shadowStyles[iter]),
+ text: shadowStyles[iter],
+ x: 500,
+ y: 20,
+ width: 100,
+ height: 100
+ });
+ stage.add_actor(label2);
+
+ resize_animated(label1);
+ resize_animated(label2);
+ GLib.timeout_add(GLib.PRIORITY_DEFAULT, DELAY, () => {
+ log(label1 + label1.get_size());
+ resize_animated(label1);
+ resize_animated(label2);
+ return true;
+ });
+
+ GLib.timeout_add(GLib.PRIORITY_DEFAULT, 2 * DELAY, () => {
+ iter += 1;
+ iter %= shadowStyles.length;
+ label1.set_style(get_css_style(shadowStyles[iter]));
+ label1.set_text(shadowStyles[iter]);
+ label2.set_style(get_css_style(shadowStyles[iter]));
+ label2.set_text(shadowStyles[iter]);
+ return true;
+ });
+
+ UI.main(stage);
+}
+test();
diff --git a/tests/interactive/box-shadows.js b/tests/interactive/box-shadows.js
new file mode 100644
index 0000000..c9c677c
--- /dev/null
+++ b/tests/interactive/box-shadows.js
@@ -0,0 +1,56 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const UI = imports.testcommon.ui;
+
+const { Clutter, St } = imports.gi;
+
+function test() {
+ let stage = new Clutter.Stage({ width: 640, height: 480 });
+ UI.init(stage);
+
+ let vbox = new St.BoxLayout({ width: stage.width,
+ height: stage.height,
+ style: 'background: #ffee88;' });
+ stage.add_actor(vbox);
+
+ let scroll = new St.ScrollView();
+ vbox.add(scroll, { expand: true });
+
+ let box = new St.BoxLayout({ vertical: true,
+ style: 'padding: 10px;'
+ + 'spacing: 20px;' });
+ scroll.add_actor(box);
+
+
+ function addTestCase(inset, offsetX, offsetY, blur, spread) {
+ let shadowStyle = 'box-shadow: ' + (inset ? 'inset ' : '') +
+ offsetX + 'px ' + offsetY + 'px ' + blur + 'px ' +
+ (spread > 0 ? (' ' + spread + 'px ') : '') +
+ 'rgba(0,0,0,0.5);';
+ let label = new St.Label({ style: 'border: 4px solid black;' +
+ 'border-radius: 5px;' +
+ 'background-color: white; ' +
+ 'padding: 5px;' +
+ shadowStyle,
+ text: shadowStyle });
+ box.add(label, { x_fill: false, y_fill: false } );
+ }
+
+ addTestCase (false, 3, 4, 0, 0);
+ addTestCase (false, 3, 4, 0, 4);
+ addTestCase (false, 3, 4, 4, 0);
+ addTestCase (false, 3, 4, 4, 4);
+ addTestCase (false, -3, -4, 4, 0);
+ addTestCase (false, 0, 0, 0, 4);
+ addTestCase (false, 0, 0, 4, 0);
+ addTestCase (true, 3, 4, 0, 0);
+ addTestCase (true, 3, 4, 0, 4);
+ addTestCase (true, 3, 4, 4, 0);
+ addTestCase (true, 3, 4, 4, 4);
+ addTestCase (true, -3, -4, 4, 0);
+ addTestCase (true, 0, 0, 0, 4);
+ addTestCase (true, 0, 0, 4, 0);
+
+ UI.main(stage);
+}
+test();
diff --git a/tests/interactive/calendar.js b/tests/interactive/calendar.js
new file mode 100644
index 0000000..d1d435a
--- /dev/null
+++ b/tests/interactive/calendar.js
@@ -0,0 +1,28 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const UI = imports.testcommon.ui;
+
+const { Clutter, St } = imports.gi;
+
+function test() {
+ let stage = new Clutter.Stage({ width: 400, height: 400 });
+ UI.init(stage);
+
+ let vbox = new St.BoxLayout({ vertical: true,
+ width: stage.width,
+ height: stage.height,
+ style: 'padding: 10px; spacing: 10px; font: 15px sans-serif;' });
+ stage.add_actor(vbox);
+
+ // Calendar can only be imported after Environment.init()
+ const Calendar = imports.ui.calendar;
+ let calendar = new Calendar.Calendar();
+ vbox.add(calendar,
+ { expand: true,
+ x_fill: false, x_align: St.Align.MIDDLE,
+ y_fill: false, y_align: St.Align.START });
+ calendar.setEventSource(new Calendar.EmptyEventSource());
+
+ UI.main(stage);
+}
+test();
diff --git a/tests/interactive/css-fonts.js b/tests/interactive/css-fonts.js
new file mode 100644
index 0000000..a257693
--- /dev/null
+++ b/tests/interactive/css-fonts.js
@@ -0,0 +1,40 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const UI = imports.testcommon.ui;
+
+const { Clutter, St } = imports.gi;
+
+function test() {
+ let stage = new Clutter.Stage();
+ UI.init(stage);
+
+ let b = new St.BoxLayout({ vertical: true,
+ width: stage.width,
+ height: stage.height });
+ stage.add_actor(b);
+
+ let t;
+
+ t = new St.Label({ "text": "Bold", style_class: "bold" });
+ b.add(t);
+ t = new St.Label({ "text": "Monospace", style_class: "monospace" });
+ b.add(t);
+ t = new St.Label({ "text": "Italic", style_class: "italic" });
+ b.add(t);
+ t = new St.Label({ "text": "Bold Italic", style_class: "bold italic" });
+ b.add(t);
+ t = new St.Label({ "text": "Big Italic", style_class: "big italic" });
+ b.add(t);
+ t = new St.Label({ "text": "Big Bold", style_class: "big bold" });
+ b.add(t);
+
+ let b2 = new St.BoxLayout({ vertical: true, style_class: "monospace" });
+ b.add(b2);
+ t = new St.Label({ "text": "Big Monospace", style_class: "big" });
+ b2.add(t);
+ t = new St.Label({ "text": "Italic Monospace", style_class: "italic" });
+ b2.add(t);
+
+ UI.main(stage);
+}
+test();
diff --git a/tests/interactive/entry.js b/tests/interactive/entry.js
new file mode 100644
index 0000000..9ae0106
--- /dev/null
+++ b/tests/interactive/entry.js
@@ -0,0 +1,57 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const UI = imports.testcommon.ui;
+
+const { Clutter, GLib, St } = imports.gi;
+
+function test() {
+ let stage = new Clutter.Stage({ width: 400, height: 400 });
+ UI.init(stage);
+
+ let vbox = new St.BoxLayout({ vertical: true,
+ width: stage.width,
+ height: stage.height,
+ style: 'padding: 10px; spacing: 10px; font: 32px sans-serif;' });
+ stage.add_actor(vbox);
+
+ let entry = new St.Entry({ style: 'border: 1px solid black; text-shadow: 0 2px red;',
+ text: 'Example text' });
+ vbox.add(entry,
+ { expand: true,
+ y_fill: false, y_align: St.Align.MIDDLE });
+ entry.grab_key_focus();
+
+ let entryTextHint = new St.Entry({ style: 'border: 1px solid black; text-shadow: 0 2px red;',
+ hint_text: 'Hint text' });
+ vbox.add(entryTextHint,
+ { expand: true,
+ y_fill: false, y_align: St.Align.MIDDLE });
+
+ let hintActor = new St.Label({ text: 'Hint actor' });
+ let entryHintActor = new St.Entry({ style: 'border: 1px solid black; text-shadow: 0 2px red;',
+ hint_actor: hintActor });
+ vbox.add(entryHintActor,
+ { expand: true,
+ y_fill: false, y_align: St.Align.MIDDLE });
+
+ let hintActor2 = new St.Label({ text: 'Hint both (actor)' });
+ let entryHintBoth = new St.Entry({ style: 'border: 1px solid black; text-shadow: 0 2px red;',
+ hint_actor: hintActor2 });
+ let idx = 0;
+ GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, function() {
+ idx++;
+
+ if (idx % 2 == 0)
+ entryHintBoth.hint_actor = hintActor2;
+ else
+ entryHintBoth.hint_text = 'Hint both (text)';
+
+ return true;
+ });
+ vbox.add(entryHintBoth,
+ { expand: true,
+ y_fill: false, y_align: St.Align.MIDDLE });
+
+ UI.main(stage);
+}
+test();
diff --git a/tests/interactive/gapplication.js b/tests/interactive/gapplication.js
new file mode 100755
index 0000000..ec38b80
--- /dev/null
+++ b/tests/interactive/gapplication.js
@@ -0,0 +1,104 @@
+#!/usr/bin/env gjs
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+imports.gi.versions = { Gdk: '3.0', Gtk: '3.0' };
+const { Gdk, Gio, GLib, Gtk } = imports.gi;
+
+function do_action(action, parameter) {
+ print ("Action '" + action.name + "' invoked");
+}
+
+function do_action_param(action, parameter) {
+ print ("Action '" + action.name + "' invoked with parameter " + parameter.print(true));
+}
+
+function do_action_toggle(action) {
+ action.set_state(GLib.Variant.new('b', !action.state.deep_unpack()));
+ print ("Toggled");
+}
+
+function do_action_state_change(action) {
+ print ("Action '" + action.name + "' has now state " + action.state.print(true));
+}
+
+function main() {
+ Gtk.init(null);
+ Gdk.set_program_class('test-gjsgapp');
+
+ let app = new Gtk.Application({ application_id: 'org.gnome.Shell.GtkApplicationTest' });
+ app.connect('activate', () => {
+ print ("Activated");
+ });
+
+ let action = new Gio.SimpleAction({ name: 'one' });
+ action.connect('activate', do_action);
+ app.add_action(action);
+
+ action = new Gio.SimpleAction({ name: 'two' });
+ action.connect('activate', do_action);
+ app.add_action(action);
+
+ action = new Gio.SimpleAction({ name: 'toggle', state: GLib.Variant.new('b', false) });
+ action.connect('activate', do_action_toggle);
+ action.connect('notify::state', do_action_state_change);
+ app.add_action(action);
+
+ action = new Gio.SimpleAction({ name: 'disable', enabled: false });
+ action.set_enabled(false);
+ action.connect('activate', do_action);
+ app.add_action(action);
+
+ action = new Gio.SimpleAction({ name: 'parameter-int', parameter_type: GLib.VariantType.new('u') });
+ action.connect('activate', do_action_param);
+ app.add_action(action);
+
+ action = new Gio.SimpleAction({ name: 'parameter-string', parameter_type: GLib.VariantType.new('s') });
+ action.connect('activate', do_action_param);
+ app.add_action(action);
+
+ let menu = new Gio.Menu();
+ menu.append('An action', 'app.one');
+
+ let section = new Gio.Menu();
+ section.append('Another action', 'app.two');
+ section.append('Same as above', 'app.two');
+ menu.append_section(null, section);
+
+ // another section, to check separators
+ section = new Gio.Menu();
+ section.append('Checkbox', 'app.toggle');
+ section.append('Disabled', 'app.disable');
+ section.append('Missing Action', 'app.no-action');
+ menu.append_section('Subsection', section);
+
+ // empty sections or submenus should be invisible
+ menu.append_section('Empty section', new Gio.Menu());
+ menu.append_submenu('Empty submenu', new Gio.Menu());
+
+ let submenu = new Gio.Menu();
+ submenu.append('Open c:\\', 'app.parameter-string::c:\\');
+ submenu.append('Open /home', 'app.parameter-string::/home');
+ menu.append_submenu('Recent files', submenu);
+
+ let item = Gio.MenuItem.new('Say 42', null);
+ item.set_action_and_target_value('app.parameter-int', GLib.Variant.new('u', 42));
+ menu.append_item(item);
+
+ item = Gio.MenuItem.new('Say 43', null);
+ item.set_action_and_target_value('app.parameter-int', GLib.Variant.new('u', 43));
+ menu.append_item(item);
+
+ let window = null;
+
+ app.connect_after('startup', app => {
+ app.set_app_menu(menu);
+ window = new Gtk.ApplicationWindow({ title: "Test Application", application: app });
+ });
+ app.connect('activate', app => {
+ window.present();
+ });
+
+ app.run(null);
+}
+
+main();
diff --git a/tests/interactive/icons.js b/tests/interactive/icons.js
new file mode 100644
index 0000000..65b7f65
--- /dev/null
+++ b/tests/interactive/icons.js
@@ -0,0 +1,79 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const UI = imports.testcommon.ui;
+
+const { Clutter, St } = imports.gi;
+
+function test() {
+ let stage = new Clutter.Stage();
+ UI.init(stage);
+
+ let b = new St.BoxLayout({ vertical: true,
+ width: stage.width,
+ height: stage.height });
+ stage.add_actor(b);
+
+ function addTest(label, icon_props) {
+ if (b.get_children().length > 0)
+ b.add (new St.BoxLayout({ style: 'background: #cccccc; border: 10px transparent white; height: 1px; ' }));
+
+ let hb = new St.BoxLayout({ vertical: false,
+ style: 'spacing: 10px;' });
+
+ hb.add(new St.Label({ text: label }), { y_fill: false });
+ hb.add(new St.Icon(icon_props));
+
+ b.add(hb);
+ }
+
+ addTest("Symbolic",
+ { icon_name: 'battery-full-symbolic',
+ icon_size: 48 });
+ addTest("Full color",
+ { icon_name: 'battery-full',
+ icon_size: 48 });
+ addTest("Default size",
+ { icon_name: 'battery-full-symbolic' });
+ addTest("Size set by property",
+ { icon_name: 'battery-full-symbolic',
+ icon_size: 32 });
+ addTest("Size set by style",
+ { icon_name: 'battery-full-symbolic',
+ style: 'icon-size: 1em;' });
+ addTest("16px icon in 48px icon widget",
+ { icon_name: 'battery-full-symbolic',
+ style: 'icon-size: 16px; width: 48px; height: 48px; border: 1px solid black;' });
+
+ function iconRow(icons, box_style) {
+ let hb = new St.BoxLayout({ vertical: false, style: box_style });
+
+ for (let iconName of icons) {
+ hb.add(new St.Icon({ icon_name: iconName,
+ icon_size: 48 }));
+ }
+
+ b.add(hb);
+ }
+
+ let normalCss = 'background: white; color: black; padding: 10px 10px;';
+ let reversedCss = 'background: black; color: white; warning-color: #ffcc00; error-color: #ff0000; padding: 10px 10px;';
+
+ let batteryIcons = ['battery-full-charging-symbolic',
+ 'battery-full-symbolic',
+ 'battery-good-symbolic',
+ 'battery-low-symbolic',
+ 'battery-caution-symbolic' ];
+
+ let volumeIcons = ['audio-volume-high-symbolic',
+ 'audio-volume-medium-symbolic',
+ 'audio-volume-low-symbolic',
+ 'audio-volume-muted-symbolic' ];
+
+ iconRow(batteryIcons, normalCss);
+ iconRow(batteryIcons, reversedCss);
+ iconRow(volumeIcons, normalCss);
+ iconRow(volumeIcons, reversedCss);
+
+ UI.main(stage);
+}
+test();
diff --git a/tests/interactive/inline-style.js b/tests/interactive/inline-style.js
new file mode 100644
index 0000000..3952c3a
--- /dev/null
+++ b/tests/interactive/inline-style.js
@@ -0,0 +1,46 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const UI = imports.testcommon.ui;
+
+const { Clutter, St } = imports.gi;
+
+function test() {
+ let stage = new Clutter.Stage();
+ UI.init(stage);
+
+ let vbox = new St.BoxLayout({ vertical: true,
+ width: stage.width,
+ height: stage.height });
+ stage.add_actor(vbox);
+
+ let hbox = new St.BoxLayout({ style: 'spacing: 12px;' });
+ vbox.add(hbox);
+
+ let text = new St.Label({ text: "Styled Text" });
+ vbox.add (text);
+
+ let size = 24;
+ function update_size() {
+ text.style = 'font-size: ' + size + 'pt';
+ }
+ update_size();
+
+ let button;
+
+ button = new St.Button ({ label: 'Smaller', style_class: 'push-button' });
+ hbox.add (button);
+ button.connect('clicked', () => {
+ size /= 1.2;
+ update_size ();
+ });
+
+ button = new St.Button ({ label: 'Bigger', style_class: 'push-button' });
+ hbox.add (button);
+ button.connect('clicked', () => {
+ size *= 1.2;
+ update_size ();
+ });
+
+ UI.main(stage);
+}
+test();
diff --git a/tests/interactive/scroll-view-sizing.js b/tests/interactive/scroll-view-sizing.js
new file mode 100644
index 0000000..a6c682e
--- /dev/null
+++ b/tests/interactive/scroll-view-sizing.js
@@ -0,0 +1,395 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const UI = imports.testcommon.ui;
+
+const { Clutter, GObject, Gtk, Shell, St } = imports.gi;
+
+// This is an interactive test of the sizing behavior of StScrollView. It
+// may be interesting in the future to split out the two classes at the
+// top into utility classes for testing the sizing behavior of other
+// containers and actors.
+
+/****************************************************************************/
+
+// FlowedBoxes: This is a simple actor that demonstrates an interesting
+// height-for-width behavior. A set of boxes of different sizes are line-wrapped
+// horizontally with the minimum horizontal size being determined by the
+// largest box. It would be easy to extend this to allow doing vertical
+// wrapping instead, if you wanted to see just how badly our width-for-height
+// implementation is or work on fixing it.
+
+const BOX_HEIGHT = 20;
+const BOX_WIDTHS = [
+ 10, 40, 100, 20, 60, 30, 70, 10, 20, 200, 50, 70, 90, 20, 40,
+ 10, 40, 100, 20, 60, 30, 70, 10, 20, 200, 50, 70, 90, 20, 40,
+ 10, 40, 100, 20, 60, 30, 70, 10, 20, 200, 50, 70, 90, 20, 40,
+ 10, 40, 100, 20, 60, 30, 70, 10, 20, 200, 50, 70, 90, 20, 40,
+];
+
+const SPACING = 10;
+
+var FlowedBoxes = GObject.registerClass(
+class FlowedBoxes extends St.Widget {
+ _init() {
+ super._init();
+
+ for (let i = 0; i < BOX_WIDTHS.length; i++) {
+ let child = new St.Bin({ width: BOX_WIDTHS[i], height: BOX_HEIGHT,
+ style: 'border: 1px solid #444444; background: #00aa44' });
+ this.add_actor(child);
+ }
+ }
+
+ vfunc_get_preferred_width(forHeight) {
+ let children = this.get_children();
+
+ let maxMinWidth = 0;
+ let totalNaturalWidth = 0;
+
+ for (let i = 0; i < children.length; i++) {
+ let child = children[i];
+ let [minWidth, naturalWidth] = child.get_preferred_width(-1);
+ maxMinWidth = Math.max(maxMinWidth, minWidth);
+ if (i != 0)
+ totalNaturalWidth += SPACING;
+ totalNaturalWidth += naturalWidth;
+ }
+
+ return [maxMinWidth, totalNaturalWidth];
+ }
+
+ _layoutChildren(forWidth, callback) {
+ let children = this.get_children();
+
+ let x = 0;
+ let y = 0;
+ for (let i = 0; i < children.length; i++) {
+ let child = children[i];
+ let [minWidth, naturalWidth] = child.get_preferred_width(-1);
+ let [minHeight, naturalHeight] = child.get_preferred_height(naturalWidth);
+
+ let x1 = x;
+ if (x != 0)
+ x1 += SPACING;
+ let x2 = x1 + naturalWidth;
+
+ if (x2 > forWidth) {
+ if (x > 0) {
+ x1 = 0;
+ y += BOX_HEIGHT + SPACING;
+ }
+
+ x2 = naturalWidth;
+ }
+
+ callback(child, x1, y, x2, y + naturalHeight);
+ x = x2;
+ }
+
+ }
+
+ vfunc_get_preferred_height(forWidth) {
+ let height = 0;
+ this._layoutChildren(forWidth,
+ function(child, x1, y1, x2, y2) {
+ height = Math.max(height, y2);
+ });
+
+ return [height, height];
+ }
+
+ vfunc_allocate(box) {
+ this.set_allocation(box);
+
+ this._layoutChildren(box.x2 - box.x1,
+ function(child, x1, y1, x2, y2) {
+ child.allocate(new Clutter.ActorBox({ x1: x1, y1: y1, x2: x2, y2: y2 }));
+ });
+ }
+});
+
+/****************************************************************************/
+
+// SizingIllustrator: this is a container that allows interactively exploring
+// the sizing behavior of the child. Lines are drawn to indicate the minimum
+// and natural size of the child, and a drag handle allows the user to resize
+// the child interactively and see how that affects it.
+//
+// This is currently only written for the case where the child is height-for-width
+
+var SizingIllustrator = GObject.registerClass(
+class SizingIllustrator extends St.Widget {
+ _init() {
+ super._init();
+
+ this.minWidthLine = new St.Bin({ style: 'background: red' });
+ this.add_actor(this.minWidthLine);
+ this.minHeightLine = new St.Bin({ style: 'background: red' });
+ this.add_actor(this.minHeightLine);
+
+ this.naturalWidthLine = new St.Bin({ style: 'background: #4444ff' });
+ this.add_actor(this.naturalWidthLine);
+ this.naturalHeightLine = new St.Bin({ style: 'background: #4444ff' });
+ this.add_actor(this.naturalHeightLine);
+
+ this.currentWidthLine = new St.Bin({ style: 'background: #aaaaaa' });
+ this.add_actor(this.currentWidthLine);
+ this.currentHeightLine = new St.Bin({ style: 'background: #aaaaaa' });
+ this.add_actor(this.currentHeightLine);
+
+ this.handle = new St.Bin({ style: 'background: yellow; border: 1px solid black;',
+ reactive: true });
+ this.handle.connect('button-press-event', this._handlePressed.bind(this));
+ this.handle.connect('button-release-event', this._handleReleased.bind(this));
+ this.handle.connect('motion-event', this._handleMotion.bind(this));
+ this.add_actor(this.handle);
+
+ this._inDrag = false;
+
+ this.width = 300;
+ this.height = 300;
+ }
+
+ add(child) {
+ this.child = child;
+ this.add_child(child);
+ this.set_child_below_sibling(child, null);
+ }
+
+ vfunc_get_preferred_width(forHeight) {
+ let children = this.get_children();
+ for (let i = 0; i < children.length; i++) {
+ let child = children[i];
+ let [minWidth, naturalWidth] = child.get_preferred_width(-1);
+ if (child == this.child) {
+ this.minWidth = minWidth;
+ this.naturalWidth = naturalWidth;
+ }
+ }
+
+ return [0, 400];
+ }
+
+ vfunc_get_preferred_height(forWidth) {
+ let children = this.get_children();
+ for (let i = 0; i < children.length; i++) {
+ let child = children[i];
+ if (child == this.child) {
+ [this.minHeight, this.naturalHeight] = child.get_preferred_height(this.width);
+ } else {
+ let [minWidth, naturalWidth] = child.get_preferred_height(naturalWidth);
+ }
+ }
+
+ return [0, 400];
+ }
+
+ vfunc_allocate(box) {
+ this.set_allocation(box);
+
+ box = this.get_theme_node().get_content_box(box);
+
+ let allocWidth = box.x2 - box.x1;
+ let allocHeight = box.y2 - box.y1;
+
+ function alloc(child, x1, y1, x2, y2) {
+ child.allocate(new Clutter.ActorBox({ x1: x1, y1: y1, x2: x2, y2: y2 }));
+ }
+
+ alloc(this.child, 0, 0, this.width, this.height);
+ alloc(this.minWidthLine, this.minWidth, 0, this.minWidth + 1, allocHeight);
+ alloc(this.naturalWidthLine, this.naturalWidth, 0, this.naturalWidth + 1, allocHeight);
+ alloc(this.currentWidthLine, this.width, 0, this.width + 1, allocHeight);
+ alloc(this.minHeightLine, 0, this.minHeight, allocWidth, this.minHeight + 1);
+ alloc(this.naturalHeightLine, 0, this.naturalHeight, allocWidth, this.naturalHeight + 1);
+ alloc(this.currentHeightLine, 0, this.height, allocWidth, this.height + 1);
+ alloc(this.handle, this.width, this.height, this.width + 10, this.height + 10);
+ }
+
+ _handlePressed(handle, event) {
+ if (event.get_button() == 1) {
+ this._inDrag = true;
+ let [handleX, handleY] = handle.get_transformed_position();
+ let [x, y] = event.get_coords();
+ this._dragX = x - handleX;
+ this._dragY = y - handleY;
+ }
+ }
+
+ _handleReleased(handle, event) {
+ if (event.get_button() == 1) {
+ this._inDrag = false;
+ }
+ }
+
+ _handleMotion(handle, event) {
+ if (this._inDrag) {
+ let [x, y] = event.get_coords();
+ let [actorX, actorY] = this.get_transformed_position();
+ this.width = x - this._dragX - actorX;
+ this.height = y - this._dragY - actorY;
+ this.queue_relayout();
+ }
+ }
+});
+
+/****************************************************************************/
+
+function test() {
+ let stage = new Clutter.Stage({ width: 600, height: 600 });
+ UI.init(stage);
+
+ let mainBox = new St.BoxLayout({ width: stage.width,
+ height: stage.height,
+ vertical: true,
+ style: 'padding: 10px;'
+ + 'spacing: 5px;'
+ + 'font: 16px sans-serif;'
+ + 'background: black;'
+ + 'color: white;' });
+ stage.add_actor(mainBox);
+
+ const DOCS = 'Red lines represent minimum size, blue lines natural size. Drag yellow handle to resize ScrollView. Click on options to change.';
+
+ let docsLabel = new St.Label({ text: DOCS });
+ docsLabel.clutter_text.line_wrap = true;
+ mainBox.add(docsLabel);
+
+ let bin = new St.Bin({ x_fill: true, y_fill: true, style: 'border: 2px solid #666666;' });
+ mainBox.add(bin, { x_fill: true, y_fill: true, expand: true });
+
+ let illustrator = new SizingIllustrator();
+ bin.add_actor(illustrator);
+
+ let scrollView = new St.ScrollView();
+ illustrator.add(scrollView);
+
+ let box = new St.BoxLayout({ vertical: true });
+ scrollView.add_actor(box);
+
+ let flowedBoxes = new FlowedBoxes();
+ box.add(flowedBoxes, { expand: false, x_fill: true, y_fill: true });
+
+ let policyBox = new St.BoxLayout({ vertical: false });
+ mainBox.add(policyBox);
+
+ policyBox.add(new St.Label({ text: 'Horizontal Policy: ' }));
+ let hpolicy = new St.Button({ label: 'AUTOMATIC', style: 'text-decoration: underline; color: #4444ff;' });
+ policyBox.add(hpolicy);
+
+ let spacer = new St.Bin();
+ policyBox.add(spacer, { expand: true });
+
+ policyBox.add(new St.Label({ text: 'Vertical Policy: '}));
+ let vpolicy = new St.Button({ label: 'AUTOMATIC', style: 'text-decoration: underline; color: #4444ff;' });
+ policyBox.add(vpolicy);
+
+ function togglePolicy(button) {
+ switch(button.label) {
+ case 'AUTOMATIC':
+ button.label = 'ALWAYS';
+ break;
+ case 'ALWAYS':
+ button.label = 'NEVER';
+ break;
+ case 'NEVER':
+ button.label = 'EXTERNAL';
+ break;
+ case 'EXTERNAL':
+ button.label = 'AUTOMATIC';
+ break;
+ }
+ scrollView.set_policy(Gtk.PolicyType[hpolicy.label], Gtk.PolicyType[vpolicy.label]);
+ }
+
+ hpolicy.connect('clicked', () => { togglePolicy(hpolicy); });
+ vpolicy.connect('clicked', () => { togglePolicy(vpolicy); });
+
+ let fadeBox = new St.BoxLayout({ vertical: false });
+ mainBox.add(fadeBox);
+
+ spacer = new St.Bin();
+ fadeBox.add(spacer, { expand: true });
+
+ fadeBox.add(new St.Label({ text: 'Padding: '}));
+ let paddingButton = new St.Button({ label: 'No', style: 'text-decoration: underline; color: #4444ff;padding-right:3px;' });
+ fadeBox.add(paddingButton);
+
+ fadeBox.add(new St.Label({ text: 'Borders: '}));
+ let borderButton = new St.Button({ label: 'No', style: 'text-decoration: underline; color: #4444ff;padding-right:3px;' });
+ fadeBox.add(borderButton);
+
+ fadeBox.add(new St.Label({ text: 'Vertical Fade: '}));
+ let vfade = new St.Button({ label: 'No', style: 'text-decoration: underline; color: #4444ff;' });
+ fadeBox.add(vfade);
+
+ fadeBox.add(new St.Label({ text: 'Overlay scrollbars: '}));
+ let overlay = new St.Button({ label: 'No', style: 'text-decoration: underline; color: #4444ff;' });
+ fadeBox.add(overlay);
+
+ function togglePadding(button) {
+ switch(button.label) {
+ case 'No':
+ button.label = 'Yes';
+ break;
+ case 'Yes':
+ button.label = 'No';
+ break;
+ }
+ if (scrollView.style == null)
+ scrollView.style = (button.label == 'Yes' ? 'padding: 10px;' : 'padding: 0;');
+ else
+ scrollView.style += (button.label == 'Yes' ? 'padding: 10px;' : 'padding: 0;');
+ }
+
+ paddingButton.connect('clicked', () => { togglePadding(paddingButton); });
+
+ function toggleBorders(button) {
+ switch(button.label) {
+ case 'No':
+ button.label = 'Yes';
+ break;
+ case 'Yes':
+ button.label = 'No';
+ break;
+ }
+ if (scrollView.style == null)
+ scrollView.style = (button.label == 'Yes' ? 'border: 2px solid red;' : 'border: 0;');
+ else
+ scrollView.style += (button.label == 'Yes' ? 'border: 2px solid red;' : 'border: 0;');
+ }
+
+ borderButton.connect('clicked', () => { toggleBorders(borderButton); });
+
+ function toggleFade(button) {
+ switch(button.label) {
+ case 'No':
+ button.label = 'Yes';
+ break;
+ case 'Yes':
+ button.label = 'No';
+ break;
+ }
+ scrollView.set_style_class_name(button.label == 'Yes' ? 'vfade' : '');
+ }
+
+ vfade.connect('clicked', () => { toggleFade(vfade); });
+ toggleFade(vfade);
+
+ function toggleOverlay(button) {
+ switch(button.label) {
+ case 'No':
+ button.label = 'Yes';
+ break;
+ case 'Yes':
+ button.label = 'No';
+ break;
+ }
+ scrollView.overlay_scrollbars = (button.label == 'Yes');
+ }
+
+ overlay.connect('clicked', () => { toggleOverlay(overlay); });
+
+ UI.main(stage);
+}
+test();
diff --git a/tests/interactive/scrolling.js b/tests/interactive/scrolling.js
new file mode 100644
index 0000000..91951ce
--- /dev/null
+++ b/tests/interactive/scrolling.js
@@ -0,0 +1,51 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const UI = imports.testcommon.ui;
+
+const { Clutter, Gtk, St } = imports.gi;
+
+function test() {
+ let stage = new Clutter.Stage();
+ UI.init(stage);
+
+ let vbox = new St.BoxLayout({ vertical: true,
+ width: stage.width,
+ height: stage.height,
+ style: "padding: 10px;" });
+ stage.add_actor(vbox);
+
+ let toggle = new St.Button({ label: 'Horizontal Scrolling',
+ toggle_mode: true });
+ vbox.add(toggle);
+
+ let v = new St.ScrollView();
+ vbox.add(v, { expand: true });
+
+ toggle.connect('notify::checked', () => {
+ v.set_policy(toggle.checked ? Gtk.PolicyType.AUTOMATIC
+ : Gtk.PolicyType.NEVER,
+ Gtk.PolicyType.AUTOMATIC);
+ });
+
+ let b = new St.BoxLayout({ vertical: true,
+ style: "border: 2px solid #880000; border-radius: 10px; padding: 0px 5px;" });
+ v.add_actor(b);
+
+ let cc_a = "a".charCodeAt(0);
+ let s = "";
+ for (let i = 0; i < 26 * 3; i++) {
+ s += String.fromCharCode(cc_a + i % 26);
+
+ let t = new St.Label({ text: s,
+ reactive: true });
+ let line = i + 1;
+ t.connect('button-press-event',
+ function() {
+ log("Click on line " + line);
+ });
+ b.add(t);
+ }
+
+ UI.main(stage);
+}
+test();
diff --git a/tests/interactive/test-title.js b/tests/interactive/test-title.js
new file mode 100755
index 0000000..0a468dd
--- /dev/null
+++ b/tests/interactive/test-title.js
@@ -0,0 +1,37 @@
+#!/usr/bin/env gjs
+
+imports.gi.versions.Gtk = '3.0';
+
+const { GLib, Gtk } = imports.gi;
+
+function nextTitle() {
+ let length = Math.random() * 20;
+ let str = '';
+
+ for (let i = 0; i < length; i++) {
+ // 97 == 'a'
+ str += String.fromCharCode(97 + Math.random() * 26);
+ }
+
+ return str;
+}
+
+function main() {
+ Gtk.init(null);
+
+ let win = new Gtk.Window({ title: nextTitle() });
+ win.connect('destroy', () => {
+ Gtk.main_quit();
+ });
+ win.present();
+
+ GLib.timeout_add(GLib.PRIORITY_DEFAULT, 5000, function() {
+ win.title = nextTitle();
+ return true;
+ });
+
+ Gtk.main();
+}
+
+main();
+
diff --git a/tests/interactive/transitions.js b/tests/interactive/transitions.js
new file mode 100644
index 0000000..7b2eac1
--- /dev/null
+++ b/tests/interactive/transitions.js
@@ -0,0 +1,35 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const UI = imports.testcommon.ui;
+
+const { Clutter, St } = imports.gi;
+
+function test() {
+ let stage = new Clutter.Stage();
+ UI.init(stage);
+
+ let hbox = new St.BoxLayout({ name: 'transition-container',
+ reactive: true,
+ track_hover: true,
+ width: stage.width,
+ height: stage.height,
+ style: 'padding: 10px;'
+ + 'spacing: 10px;' });
+ stage.add_actor(hbox);
+
+ for (let i = 0; i < 5; i ++) {
+ let label = new St.Label({ text: (i+1).toString(),
+ name: "label" + i,
+ style_class: 'transition-label',
+ reactive: true,
+ track_hover: true });
+
+ hbox.add(label, { x_fill: false,
+ y_fill: false });
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+
+ UI.main(stage);
+}
+test();
diff --git a/tests/meson.build b/tests/meson.build
new file mode 100644
index 0000000..1e84f42
--- /dev/null
+++ b/tests/meson.build
@@ -0,0 +1,18 @@
+testconf = configuration_data()
+testconf.set('MUTTER_TYPELIB_DIR', mutter_typelibdir)
+testconf.set('srcdir', meson.current_source_dir())
+run_test = configure_file(
+ input: 'run-test.sh.in',
+ output: 'run-test.sh',
+ configuration: testconf
+)
+
+testenv = environment()
+testenv.set('GSETTINGS_SCHEMA_DIR', join_paths(meson.build_root(), 'data'))
+
+foreach test : ['insertSorted', 'jsParse', 'markup', 'params', 'url']
+ test(test, run_test,
+ args: 'unit/@0@.js'.format(test),
+ env: testenv,
+ workdir: meson.current_source_dir())
+endforeach
diff --git a/tests/run-test.sh.in b/tests/run-test.sh.in
new file mode 100755
index 0000000..ea6d157
--- /dev/null
+++ b/tests/run-test.sh.in
@@ -0,0 +1,45 @@
+#!/bin/sh
+
+usage() {
+ echo >&2 "Usage run-test.sh [-v|--verbose] <test_js>..."
+ exit 1
+}
+
+tests=
+verbose=false
+debug=
+for arg in $@ ; do
+ case $arg in
+ -g|--debug)
+ debug="libtool --mode=execute gdb --args"
+ ;;
+ -v|--verbose)
+ verbose=true
+ ;;
+ -*)
+ usage
+ ;;
+ *)
+ tests="$tests $arg"
+ ;;
+ esac
+done
+
+builddir=`dirname $0`
+builddir=`cd $builddir && pwd`
+srcdir=@srcdir@
+srcdir=`cd $srcdir && pwd`
+
+GI_TYPELIB_PATH="$GI_TYPELIB_PATH${GI_TYPELIB_PATH:+:}@MUTTER_TYPELIB_DIR@:$builddir/../src:$builddir/../src/st:$builddir/../subprojects/gvc"
+GJS_PATH="$srcdir:$srcdir/../js:$builddir/../js"
+GJS_DEBUG_OUTPUT=stderr
+$verbose || GJS_DEBUG_TOPICS="JS ERROR;JS LOG"
+GNOME_SHELL_TESTSDIR="$srcdir/"
+GNOME_SHELL_JS="$srcdir/../js"
+GNOME_SHELL_DATADIR="$builddir/../data"
+
+export GI_TYPELIB_PATH GJS_PATH GJS_DEBUG_OUTPUT GJS_DEBUG_TOPICS GNOME_SHELL_TESTSDIR GNOME_SHELL_JS GNOME_SHELL_DATADIR LD_PRELOAD
+
+for test in $tests ; do
+ $debug $builddir/../src/run-js-test $test || exit $?
+done
diff --git a/tests/testcommon/100-200.svg b/tests/testcommon/100-200.svg
new file mode 100644
index 0000000..59a5307
--- /dev/null
+++ b/tests/testcommon/100-200.svg
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 100 200" width="100" height="200">
+ <path
+ d="
+ M 2,2 h 96 v 196 h -96 v -196
+ M 8,8 h 84 v 184 h -84 v -184
+ "
+ fill="white"
+ stroke="blue"
+ stroke-width="2"
+ stroke-linecap="square"
+ />
+ <path
+ d="
+ M 10,10 h 20 v 20 h -20 v -20
+ "
+ fill="green"
+ />
+</svg>
diff --git a/tests/testcommon/200-100.svg b/tests/testcommon/200-100.svg
new file mode 100644
index 0000000..e149b5f
--- /dev/null
+++ b/tests/testcommon/200-100.svg
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 200 100" width="200" height="100">
+ <path
+ d="
+ M 2,2 h 196 v 96 h -196 v -96
+ M 8,8 h 184 v 84 h -184 v -84
+ "
+ fill="white"
+ stroke="blue"
+ stroke-width="2"
+ stroke-linecap="square"
+ />
+ <path
+ d="
+ M 10,10 h 20 v 20 h -20 v -20
+ "
+ fill="green"
+ />
+</svg>
diff --git a/tests/testcommon/200-200.svg b/tests/testcommon/200-200.svg
new file mode 100644
index 0000000..9965a2a
--- /dev/null
+++ b/tests/testcommon/200-200.svg
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 200 200" width="200" height="200">
+ <path
+ d="
+ M 2,2 h 196 v 196 h -196 v -196
+ M 8,8 h 184 v 184 h -184 v -184
+ "
+ fill="white"
+ stroke="blue"
+ stroke-width="2"
+ stroke-linecap="square"
+ />
+ <path
+ d="
+ M 10,10 h 20 v 20 h -20 v -20
+ "
+ fill="green"
+ />
+</svg>
diff --git a/tests/testcommon/border-image.png b/tests/testcommon/border-image.png
new file mode 100644
index 0000000..e680020
--- /dev/null
+++ b/tests/testcommon/border-image.png
Binary files differ
diff --git a/tests/testcommon/face-plain.png b/tests/testcommon/face-plain.png
new file mode 100644
index 0000000..962d70f
--- /dev/null
+++ b/tests/testcommon/face-plain.png
Binary files differ
diff --git a/tests/testcommon/test.css b/tests/testcommon/test.css
new file mode 100644
index 0000000..b82d230
--- /dev/null
+++ b/tests/testcommon/test.css
@@ -0,0 +1,112 @@
+@import url("resource:///org/gnome/shell/theme/gnome-shell.css");
+
+stage {
+ font: 16pt serif;
+ color: black;
+}
+
+.red {
+ background-color: red;
+}
+
+.green {
+ background-color: green;
+}
+
+.blue {
+ background-color: blue;
+}
+
+.bold {
+ font-weight: bold;
+}
+
+.italic {
+ font-style: italic;
+}
+
+.big {
+ font-size: 150%;
+}
+
+.monospace {
+ font-family: monospace;
+}
+
+.border-image {
+ border: 15px;
+ border-image: url('border-image.png') 16;
+}
+
+.background-image-200-200 {
+ background-image: url('200-200.svg');
+}
+
+.background-image-100-200 {
+ background-image: url('100-200.svg');
+}
+
+.background-image-200-100 {
+ background-image: url('200-100.svg');
+}
+
+.background-gradient {
+ background-gradient-start: rgba(127, 255, 127, .6);
+ background-gradient-end: rgba(127, 127, 255, .6);
+}
+
+.border-image-with-background-gradient {
+ border: 15px black solid;
+ border-image: url('border-image.png') 16;
+ background-gradient-start: #88ff88;
+ background-gradient-end: #8888ff;
+}
+
+.background-image {
+ background-image: url('face-plain.png');
+ background-color: white;
+}
+
+.background-repeat {
+ background-repeat: repeat;
+}
+
+.push-button {
+ background: #eeddbb;
+ border: 1px solid black;
+ border-radius: 8px;
+ padding: 5px;
+}
+
+.push-button:hover {
+ background: #ffeecc;
+}
+
+.push-button:active {
+ background: #ccbb99;
+}
+
+.vfade {
+ -st-fade-offset: 68px;
+}
+
+#transition-container .transition-label {
+ color: white;
+ width: 1em;
+ height: 1em;
+ padding: 1em;
+ background-color: #333;
+ border: 2px solid black;
+ border-radius: 8px;
+ transition-duration: 1s;
+}
+
+#transition-container:hover .transition-label {
+ background-color: blue;
+ border: 2px solid red;
+}
+
+#transition-container .transition-label:hover {
+ background-color: green;
+ border: 2px solid blue;
+}
diff --git a/tests/testcommon/ui.js b/tests/testcommon/ui.js
new file mode 100644
index 0000000..abacea5
--- /dev/null
+++ b/tests/testcommon/ui.js
@@ -0,0 +1,28 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Config = imports.misc.config;
+
+imports.gi.versions = { Clutter: Config.LIBMUTTER_API_VERSION, Gtk: '3.0' };
+
+const { Clutter, Gio, GLib, St } = imports.gi;
+
+const Environment = imports.ui.environment;
+
+function init(stage) {
+ Environment.init();
+ let themeResource = Gio.Resource.load(global.datadir + '/gnome-shell-theme.gresource');
+ themeResource._register();
+
+ let context = St.ThemeContext.get_for_stage(stage);
+ let stylesheetPath = GLib.getenv("GNOME_SHELL_TESTSDIR") + "/testcommon/test.css";
+ let theme = new St.Theme({ application_stylesheet: Gio.File.new_for_path(stylesheetPath) });
+ context.set_theme(theme);
+}
+
+function main(stage) {
+ stage.show();
+ stage.connect('destroy', () => {
+ Clutter.main_quit();
+ });
+ Clutter.main();
+}
diff --git a/tests/unit/insertSorted.js b/tests/unit/insertSorted.js
new file mode 100644
index 0000000..610aeed
--- /dev/null
+++ b/tests/unit/insertSorted.js
@@ -0,0 +1,76 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+// Test cases for Util.insertSorted
+
+const JsUnit = imports.jsUnit;
+
+// Needed so that Util can bring some UI stuff
+// we don't actually use
+const Environment = imports.ui.environment;
+Environment.init();
+const Util = imports.misc.util;
+
+function assertArrayEquals(errorMessage, array1, array2) {
+ JsUnit.assertEquals(errorMessage + ' length',
+ array1.length, array2.length);
+ for (let j = 0; j < array1.length; j++) {
+ JsUnit.assertEquals(errorMessage + ' item ' + j,
+ array1[j], array2[j]);
+ }
+}
+
+function cmp(one, two) {
+ return one-two;
+}
+
+let arrayInt = [1, 2, 3, 5, 6];
+Util.insertSorted(arrayInt, 4, cmp);
+
+assertArrayEquals('first test', [1,2,3,4,5,6], arrayInt);
+
+// no comparator, integer sorting is implied
+Util.insertSorted(arrayInt, 3);
+
+assertArrayEquals('second test', [1,2,3,3,4,5,6], arrayInt);
+
+let obj1 = { a: 1 };
+let obj2 = { a: 2, b: 0 };
+let obj3 = { a: 2, b: 1 };
+let obj4 = { a: 3 };
+
+function objCmp(one, two) {
+ return one.a - two.a;
+}
+
+let arrayObj = [obj1, obj3, obj4];
+
+// obj2 compares equivalent to obj3, should be
+// inserted before
+Util.insertSorted(arrayObj, obj2, objCmp);
+
+assertArrayEquals('object test', [obj1, obj2, obj3, obj4], arrayObj);
+
+function checkedCmp(one, two) {
+ if (typeof one != 'number' ||
+ typeof two != 'number')
+ throw new TypeError('Invalid type passed to checkedCmp');
+
+ return one-two;
+}
+
+let arrayEmpty = [];
+
+// check that no comparisons are made when
+// inserting in a empty array
+Util.insertSorted(arrayEmpty, 3, checkedCmp);
+
+// Insert at the end and check that we don't
+// access past it
+Util.insertSorted(arrayEmpty, 4, checkedCmp);
+Util.insertSorted(arrayEmpty, 5, checkedCmp);
+
+// Some more insertions
+Util.insertSorted(arrayEmpty, 2, checkedCmp);
+Util.insertSorted(arrayEmpty, 1, checkedCmp);
+
+assertArrayEquals('checkedCmp test', [1, 2, 3, 4, 5], arrayEmpty);
diff --git a/tests/unit/jsParse.js b/tests/unit/jsParse.js
new file mode 100644
index 0000000..468138b
--- /dev/null
+++ b/tests/unit/jsParse.js
@@ -0,0 +1,194 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+// Test cases for MessageTray URLification
+
+const JsUnit = imports.jsUnit;
+
+const Environment = imports.ui.environment;
+Environment.init();
+
+const JsParse = imports.misc.jsParse;
+
+const HARNESS_COMMAND_HEADER = "let imports = obj;" +
+ "let global = obj;" +
+ "let Main = obj;" +
+ "let foo = obj;" +
+ "let r = obj;";
+
+const testsFindMatchingQuote = [
+ { input: '"double quotes"',
+ output: 0 },
+ { input: '\'single quotes\'',
+ output: 0 },
+ { input: 'some unquoted "some quoted"',
+ output: 14 },
+ { input: '"mixed \' quotes\'"',
+ output: 0 },
+ { input: '"escaped \\" quote"',
+ output: 0 }
+];
+const testsFindMatchingSlash = [
+ { input: '/slash/',
+ output: 0 },
+ { input: '/slash " with $ funny ^\' stuff/',
+ output: 0 },
+ { input: 'some unslashed /some slashed/',
+ output: 15 },
+ { input: '/escaped \\/ slash/',
+ output: 0 }
+];
+const testsFindMatchingBrace = [
+ { input: '[square brace]',
+ output: 0 },
+ { input: '(round brace)',
+ output: 0 },
+ { input: '([()][nesting!])',
+ output: 0 },
+ { input: '[we have "quoted [" braces]',
+ output: 0 },
+ { input: '[we have /regex [/ braces]',
+ output: 0 },
+ { input: '([[])[] mismatched braces ]',
+ output: 1 }
+];
+const testsGetExpressionOffset = [
+ { input: 'abc.123',
+ output: 0 },
+ { input: 'foo().bar',
+ output: 0 },
+ { input: 'foo(bar',
+ output: 4 },
+ { input: 'foo[abc.match(/"/)]',
+ output: 0 }
+];
+const testsGetDeclaredConstants = [
+ { input: 'const foo = X; const bar = Y;',
+ output: ['foo', 'bar'] },
+ { input: 'const foo=X; const bar=Y',
+ output: ['foo', 'bar'] }
+];
+const testsIsUnsafeExpression = [
+ { input: 'foo.bar',
+ output: false },
+ { input: 'foo[\'bar\']',
+ output: false },
+ { input: 'foo["a=b=c".match(/=/)',
+ output: false },
+ { input: 'foo[1==2]',
+ output: false },
+ { input: '(x=4)',
+ output: true },
+ { input: '(x = 4)',
+ output: true },
+ { input: '(x;y)',
+ output: true }
+];
+const testsModifyScope = [
+ "foo['a",
+ "foo()['b'",
+ "obj.foo()('a', 1, 2, 'b')().",
+ "foo.[.",
+ "foo]]]()))].",
+ "123'ab\"",
+ "Main.foo.bar = 3; bar.",
+ "(Main.foo = 3).",
+ "Main[Main.foo+=-1]."
+];
+
+
+
+// Utility function for comparing arrays
+function assertArrayEquals(errorMessage, array1, array2) {
+ JsUnit.assertEquals(errorMessage + ' length',
+ array1.length, array2.length);
+ for (let j = 0; j < array1.length; j++) {
+ JsUnit.assertEquals(errorMessage + ' item ' + j,
+ array1[j], array2[j]);
+ }
+}
+
+//
+// Test javascript parsing
+//
+
+for (let i = 0; i < testsFindMatchingQuote.length; i++) {
+ let text = testsFindMatchingQuote[i].input;
+ let match = JsParse.findMatchingQuote(text, text.length - 1);
+
+ JsUnit.assertEquals('Test testsFindMatchingQuote ' + i,
+ match, testsFindMatchingQuote[i].output);
+}
+
+for (let i = 0; i < testsFindMatchingSlash.length; i++) {
+ let text = testsFindMatchingSlash[i].input;
+ let match = JsParse.findMatchingSlash(text, text.length - 1);
+
+ JsUnit.assertEquals('Test testsFindMatchingSlash ' + i,
+ match, testsFindMatchingSlash[i].output);
+}
+
+for (let i = 0; i < testsFindMatchingBrace.length; i++) {
+ let text = testsFindMatchingBrace[i].input;
+ let match = JsParse.findMatchingBrace(text, text.length - 1);
+
+ JsUnit.assertEquals('Test testsFindMatchingBrace ' + i,
+ match, testsFindMatchingBrace[i].output);
+}
+
+for (let i = 0; i < testsGetExpressionOffset.length; i++) {
+ let text = testsGetExpressionOffset[i].input;
+ let match = JsParse.getExpressionOffset(text, text.length - 1);
+
+ JsUnit.assertEquals('Test testsGetExpressionOffset ' + i,
+ match, testsGetExpressionOffset[i].output);
+}
+
+for (let i = 0; i < testsGetDeclaredConstants.length; i++) {
+ let text = testsGetDeclaredConstants[i].input;
+ let match = JsParse.getDeclaredConstants(text);
+
+ assertArrayEquals('Test testsGetDeclaredConstants ' + i,
+ match, testsGetDeclaredConstants[i].output);
+}
+
+for (let i = 0; i < testsIsUnsafeExpression.length; i++) {
+ let text = testsIsUnsafeExpression[i].input;
+ let unsafe = JsParse.isUnsafeExpression(text);
+
+ JsUnit.assertEquals('Test testsIsUnsafeExpression ' + i,
+ unsafe, testsIsUnsafeExpression[i].output);
+}
+
+//
+// Test safety of eval to get completions
+//
+
+for (let i = 0; i < testsModifyScope.length; i++) {
+ let text = testsModifyScope[i];
+ // We need to use var here for the with statement
+ var obj = {};
+
+ // Just as in JsParse.getCompletions, we will find the offset
+ // of the expression, test whether it is unsafe, and then eval it.
+ let offset = JsParse.getExpressionOffset(text, text.length - 1);
+ if (offset >= 0) {
+ text = text.slice(offset);
+
+ let matches = text.match(/(.*)\.(.*)/);
+ if (matches) {
+ let [expr, base, attrHead] = matches;
+
+ if (!JsParse.isUnsafeExpression(base)) {
+ with (obj) {
+ try {
+ eval(HARNESS_COMMAND_HEADER + base);
+ } catch (e) {
+ JsUnit.assertNotEquals("Code '" + base + "' is valid code", e.constructor, SyntaxError);
+ }
+ }
+ }
+ }
+ }
+ let propertyNames = Object.getOwnPropertyNames(obj);
+ JsUnit.assertEquals("The context '" + JSON.stringify(obj) + "' was not modified", propertyNames.length, 0);
+}
diff --git a/tests/unit/markup.js b/tests/unit/markup.js
new file mode 100644
index 0000000..603ca81
--- /dev/null
+++ b/tests/unit/markup.js
@@ -0,0 +1,143 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+// Test cases for MessageList markup parsing
+
+const JsUnit = imports.jsUnit;
+const Pango = imports.gi.Pango;
+
+const Environment = imports.ui.environment;
+Environment.init();
+
+const Main = imports.ui.main; // unused, but needed to break dependency loop
+const MessageList = imports.ui.messageList;
+
+// Assert that @input, assumed to be markup, gets "fixed" to @output,
+// which is valid markup. If @output is null, @input is expected to
+// convert to itself
+function assertConverts(input, output) {
+ if (!output)
+ output = input;
+ let fixed = MessageList._fixMarkup(input, true);
+ JsUnit.assertEquals(output, fixed);
+
+ let parsed = false;
+ try {
+ Pango.parse_markup(fixed, -1, '');
+ parsed = true;
+ } catch (e) {}
+ JsUnit.assertEquals(true, parsed);
+}
+
+// Assert that @input, assumed to be plain text, gets escaped to @output,
+// which is valid markup.
+function assertEscapes(input, output) {
+ let fixed = MessageList._fixMarkup(input, false);
+ JsUnit.assertEquals(output, fixed);
+
+ let parsed = false;
+ try {
+ Pango.parse_markup(fixed, -1, '');
+ parsed = true;
+ } catch (e) {}
+ JsUnit.assertEquals(true, parsed);
+}
+
+
+
+// CORRECT MARKUP
+
+assertConverts('foo');
+assertEscapes('foo', 'foo');
+
+assertConverts('<b>foo</b>');
+assertEscapes('<b>foo</b>', '&lt;b&gt;foo&lt;/b&gt;');
+
+assertConverts('something <i>foo</i>');
+assertEscapes('something <i>foo</i>', 'something &lt;i&gt;foo&lt;/i&gt;');
+
+assertConverts('<u>foo</u> something');
+assertEscapes('<u>foo</u> something', '&lt;u&gt;foo&lt;/u&gt; something');
+
+assertConverts('<b>bold</b> <i>italic <u>and underlined</u></i>');
+assertEscapes('<b>bold</b> <i>italic <u>and underlined</u></i>', '&lt;b&gt;bold&lt;/b&gt; &lt;i&gt;italic &lt;u&gt;and underlined&lt;/u&gt;&lt;/i&gt;');
+
+assertConverts('this &amp; that');
+assertEscapes('this &amp; that', 'this &amp;amp; that');
+
+assertConverts('this &lt; that');
+assertEscapes('this &lt; that', 'this &amp;lt; that');
+
+assertConverts('this &lt; that &gt; the other');
+assertEscapes('this &lt; that &gt; the other', 'this &amp;lt; that &amp;gt; the other');
+
+assertConverts('this &lt;<i>that</i>&gt;');
+assertEscapes('this &lt;<i>that</i>&gt;', 'this &amp;lt;&lt;i&gt;that&lt;/i&gt;&amp;gt;');
+
+assertConverts('<b>this</b> > <i>that</i>');
+assertEscapes('<b>this</b> > <i>that</i>', '&lt;b&gt;this&lt;/b&gt; &gt; &lt;i&gt;that&lt;/i&gt;');
+
+
+
+// PARTIALLY CORRECT MARKUP
+// correct bits are kept, incorrect bits are escaped
+
+// unrecognized entity
+assertConverts('<b>smile</b> &#9786;!', '<b>smile</b> &amp;#9786;!');
+assertEscapes('<b>smile</b> &#9786;!', '&lt;b&gt;smile&lt;/b&gt; &amp;#9786;!');
+
+// stray '&'; this is really a bug, but it's easier to do it this way
+assertConverts('<b>this</b> & <i>that</i>', '<b>this</b> &amp; <i>that</i>');
+assertEscapes('<b>this</b> & <i>that</i>', '&lt;b&gt;this&lt;/b&gt; &amp; &lt;i&gt;that&lt;/i&gt;');
+
+// likewise with stray '<'
+assertConverts('this < that', 'this &lt; that');
+assertEscapes('this < that', 'this &lt; that');
+
+assertConverts('<b>this</b> < <i>that</i>', '<b>this</b> &lt; <i>that</i>');
+assertEscapes('<b>this</b> < <i>that</i>', '&lt;b&gt;this&lt;/b&gt; &lt; &lt;i&gt;that&lt;/i&gt;');
+
+assertConverts('this < that > the other', 'this &lt; that > the other');
+assertEscapes('this < that > the other', 'this &lt; that &gt; the other');
+
+assertConverts('this <<i>that</i>>', 'this &lt;<i>that</i>>');
+assertEscapes('this <<i>that</i>>', 'this &lt;&lt;i&gt;that&lt;/i&gt;&gt;');
+
+// unknown tags
+assertConverts('<unknown>tag</unknown>', '&lt;unknown>tag&lt;/unknown>');
+assertEscapes('<unknown>tag</unknown>', '&lt;unknown&gt;tag&lt;/unknown&gt;');
+
+// make sure we check beyond the first letter
+assertConverts('<bunknown>tag</bunknown>', '&lt;bunknown>tag&lt;/bunknown>');
+assertEscapes('<bunknown>tag</bunknown>', '&lt;bunknown&gt;tag&lt;/bunknown&gt;');
+
+// with mix of good and bad, we keep the good and escape the bad
+assertConverts('<i>known</i> and <unknown>tag</unknown>', '<i>known</i> and &lt;unknown>tag&lt;/unknown>');
+assertEscapes('<i>known</i> and <unknown>tag</unknown>', '&lt;i&gt;known&lt;/i&gt; and &lt;unknown&gt;tag&lt;/unknown&gt;');
+
+
+
+// FULLY INCORRECT MARKUP
+// (fall back to escaping the whole thing)
+
+// tags not matched up
+assertConverts('<b>in<i>com</i>plete', '&lt;b&gt;in&lt;i&gt;com&lt;/i&gt;plete');
+assertEscapes('<b>in<i>com</i>plete', '&lt;b&gt;in&lt;i&gt;com&lt;/i&gt;plete');
+
+assertConverts('in<i>com</i>plete</b>', 'in&lt;i&gt;com&lt;/i&gt;plete&lt;/b&gt;');
+assertEscapes('in<i>com</i>plete</b>', 'in&lt;i&gt;com&lt;/i&gt;plete&lt;/b&gt;');
+
+// we don't support attributes, and it's too complicated to try
+// to escape both start and end tags, so we just treat it as bad
+assertConverts('<b>good</b> and <b style=\'bad\'>bad</b>', '&lt;b&gt;good&lt;/b&gt; and &lt;b style=&apos;bad&apos;&gt;bad&lt;/b&gt;');
+assertEscapes('<b>good</b> and <b style=\'bad\'>bad</b>', '&lt;b&gt;good&lt;/b&gt; and &lt;b style=&apos;bad&apos;&gt;bad&lt;/b&gt;');
+
+// this is just syntactically invalid
+assertConverts('<b>unrecognized</b stuff>', '&lt;b&gt;unrecognized&lt;/b stuff&gt;');
+assertEscapes('<b>unrecognized</b stuff>', '&lt;b&gt;unrecognized&lt;/b stuff&gt;');
+
+// mismatched tags
+assertConverts('<b>mismatched</i>', '&lt;b&gt;mismatched&lt;/i&gt;');
+assertEscapes('<b>mismatched</i>', '&lt;b&gt;mismatched&lt;/i&gt;');
+
+assertConverts('<b>mismatched/unknown</bunknown>', '&lt;b&gt;mismatched/unknown&lt;/bunknown&gt;');
+assertEscapes('<b>mismatched/unknown</bunknown>', '&lt;b&gt;mismatched/unknown&lt;/bunknown&gt;');
diff --git a/tests/unit/params.js b/tests/unit/params.js
new file mode 100644
index 0000000..6ac4cc1
--- /dev/null
+++ b/tests/unit/params.js
@@ -0,0 +1,32 @@
+const JsUnit = imports.jsUnit;
+const Params = imports.misc.params;
+
+function assertParamsEqual(params, expected) {
+ for (let p in params) {
+ JsUnit.assertTrue(p in expected);
+ JsUnit.assertEquals(params[p], expected[p]);
+ }
+}
+
+let defaults = {
+ foo: 'This is a test',
+ bar: null,
+ baz: 42
+};
+
+assertParamsEqual(
+ Params.parse(null, defaults),
+ defaults);
+
+assertParamsEqual(
+ Params.parse({ bar: 23 }, defaults),
+ { foo: 'This is a test', bar: 23, baz: 42 });
+
+JsUnit.assertRaises(
+ () => {
+ Params.parse({ extraArg: 'quz' }, defaults);
+ });
+
+assertParamsEqual(
+ Params.parse({ extraArg: 'quz' }, defaults, true),
+ { foo: 'This is a test', bar: null, baz: 42, extraArg: 'quz' });
diff --git a/tests/unit/url.js b/tests/unit/url.js
new file mode 100644
index 0000000..84aecc9
--- /dev/null
+++ b/tests/unit/url.js
@@ -0,0 +1,77 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+// Test cases for MessageTray URLification
+
+const JsUnit = imports.jsUnit;
+
+const Environment = imports.ui.environment;
+Environment.init();
+
+const Util = imports.misc.util;
+
+const tests = [
+ { input: 'This is a test',
+ output: [] },
+ { input: 'This is http://www.gnome.org a test',
+ output: [ { url: 'http://www.gnome.org', pos: 8 } ] },
+ { input: 'This is http://www.gnome.org',
+ output: [ { url: 'http://www.gnome.org', pos: 8 } ] },
+ { input: 'http://www.gnome.org a test',
+ output: [ { url: 'http://www.gnome.org', pos: 0 } ] },
+ { input: 'http://www.gnome.org',
+ output: [ { url: 'http://www.gnome.org', pos: 0 } ] },
+ { input: 'Go to http://www.gnome.org.',
+ output: [ { url: 'http://www.gnome.org', pos: 6 } ] },
+ { input: 'Go to http://www.gnome.org/.',
+ output: [ { url: 'http://www.gnome.org/', pos: 6 } ] },
+ { input: '(Go to http://www.gnome.org!)',
+ output: [ { url: 'http://www.gnome.org', pos: 7 } ] },
+ { input: 'Use GNOME (http://www.gnome.org).',
+ output: [ { url: 'http://www.gnome.org', pos: 11 } ] },
+ { input: 'This is a http://www.gnome.org/path test.',
+ output: [ { url: 'http://www.gnome.org/path', pos: 10 } ] },
+ { input: 'This is a www.gnome.org scheme-less test.',
+ output: [ { url: 'www.gnome.org', pos: 10 } ] },
+ { input: 'This is a www.gnome.org/scheme-less test.',
+ output: [ { url: 'www.gnome.org/scheme-less', pos: 10 } ] },
+ { input: 'This is a http://www.gnome.org:99/port test.',
+ output: [ { url: 'http://www.gnome.org:99/port', pos: 10 } ] },
+ { input: 'This is an ftp://www.gnome.org/ test.',
+ output: [ { url: 'ftp://www.gnome.org/', pos: 11 } ] },
+ { input: 'https://www.gnome.org/(some_url,_with_very_unusual_characters)',
+ output: [ { url: 'https://www.gnome.org/(some_url,_with_very_unusual_characters)', pos: 0 } ] },
+ { input: 'https://www.gnome.org/(some_url_with_unbalanced_parenthesis',
+ output: [ { url: 'https://www.gnome.org/', pos: 0 } ] },
+ { input: 'https://www.gnome.org/‎ plus trailing junk',
+ output: [ { url: 'https://www.gnome.org/', pos: 0 } ] },
+
+ { input: 'Visit http://www.gnome.org/ and http://developer.gnome.org',
+ output: [ { url: 'http://www.gnome.org/', pos: 6 },
+ { url: 'http://developer.gnome.org', pos: 32 } ] },
+
+ { input: 'This is not.a.domain test.',
+ output: [ ] },
+ { input: 'This is not:a.url test.',
+ output: [ ] },
+ { input: 'This is not:/a.url/ test.',
+ output: [ ] },
+ { input: 'This is not:/a.url/ test.',
+ output: [ ] },
+ { input: 'This is not@a.url/ test.',
+ output: [ ] },
+ { input: 'This is surely@not.a/url test.',
+ output: [ ] }
+];
+
+for (let i = 0; i < tests.length; i++) {
+ let match = Util.findUrls(tests[i].input);
+
+ JsUnit.assertEquals('Test ' + i + ' match length',
+ match.length, tests[i].output.length);
+ for (let j = 0; j < match.length; j++) {
+ JsUnit.assertEquals('Test ' + i + ', match ' + j + ' url',
+ match[j].url, tests[i].output[j].url);
+ JsUnit.assertEquals('Test ' + i + ', match ' + j + ' position',
+ match[j].pos, tests[i].output[j].pos);
+ }
+}