From e4283f6d48b98e764b988b43bbc86b9d52e6ec94 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:54:43 +0200 Subject: Adding upstream version 43.9. Signed-off-by: Daniel Baumann --- tests/interactive/background-repeat.js | 28 +++ tests/interactive/background-size.js | 82 +++++++ tests/interactive/border-radius.js | 61 +++++ tests/interactive/border-width.js | 58 +++++ tests/interactive/borders.js | 133 +++++++++++ tests/interactive/box-layout.js | 85 +++++++ tests/interactive/box-shadow-animated.js | 80 +++++++ tests/interactive/box-shadows.js | 56 +++++ tests/interactive/calendar.js | 28 +++ tests/interactive/css-fonts.js | 40 ++++ tests/interactive/entry.js | 57 +++++ tests/interactive/gapplication.js | 104 ++++++++ tests/interactive/icons.js | 79 +++++++ tests/interactive/inline-style.js | 46 ++++ tests/interactive/scroll-view-sizing.js | 395 +++++++++++++++++++++++++++++++ tests/interactive/scrolling.js | 51 ++++ tests/interactive/test-title.js | 37 +++ tests/interactive/transitions.js | 35 +++ tests/meson.build | 29 +++ tests/run-test.sh.in | 45 ++++ tests/testcommon/100-200.svg | 21 ++ tests/testcommon/200-100.svg | 21 ++ tests/testcommon/200-200.svg | 21 ++ tests/testcommon/border-image.png | Bin 0 -> 981 bytes tests/testcommon/face-plain.png | Bin 0 -> 4298 bytes tests/testcommon/test.css | 112 +++++++++ tests/testcommon/ui.js | 28 +++ tests/unit/highlighter.js | 106 +++++++++ tests/unit/insertSorted.js | 76 ++++++ tests/unit/jsParse.js | 194 +++++++++++++++ tests/unit/markup.js | 143 +++++++++++ tests/unit/params.js | 32 +++ tests/unit/signalTracker.js | 115 +++++++++ tests/unit/url.js | 77 ++++++ tests/unit/versionCompare.js | 52 ++++ 35 files changed, 2527 insertions(+) create mode 100644 tests/interactive/background-repeat.js create mode 100644 tests/interactive/background-size.js create mode 100644 tests/interactive/border-radius.js create mode 100644 tests/interactive/border-width.js create mode 100644 tests/interactive/borders.js create mode 100644 tests/interactive/box-layout.js create mode 100644 tests/interactive/box-shadow-animated.js create mode 100644 tests/interactive/box-shadows.js create mode 100644 tests/interactive/calendar.js create mode 100644 tests/interactive/css-fonts.js create mode 100644 tests/interactive/entry.js create mode 100755 tests/interactive/gapplication.js create mode 100644 tests/interactive/icons.js create mode 100644 tests/interactive/inline-style.js create mode 100644 tests/interactive/scroll-view-sizing.js create mode 100644 tests/interactive/scrolling.js create mode 100755 tests/interactive/test-title.js create mode 100644 tests/interactive/transitions.js create mode 100644 tests/meson.build create mode 100755 tests/run-test.sh.in create mode 100644 tests/testcommon/100-200.svg create mode 100644 tests/testcommon/200-100.svg create mode 100644 tests/testcommon/200-200.svg create mode 100644 tests/testcommon/border-image.png create mode 100644 tests/testcommon/face-plain.png create mode 100644 tests/testcommon/test.css create mode 100644 tests/testcommon/ui.js create mode 100644 tests/unit/highlighter.js create mode 100644 tests/unit/insertSorted.js create mode 100644 tests/unit/jsParse.js create mode 100644 tests/unit/markup.js create mode 100644 tests/unit/params.js create mode 100644 tests/unit/signalTracker.js create mode 100644 tests/unit/url.js create mode 100644 tests/unit/versionCompare.js (limited to 'tests') 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..8f8738d --- /dev/null +++ b/tests/interactive/background-size.js @@ -0,0 +1,82 @@ +// -*- 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) { + let obin = new St.Bin({ style: 'border: 3px solid green;' }); + tbox.add(obin); + + let [width, height] = size; + let bin = new St.Bin({ style_class: 'background-image-' + image, + width: width, + height: height, + style: `${useCairo + ? '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) { + 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..9dd2a40 --- /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.deepUnpack())); + 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..9d3925d --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,29 @@ +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.project_build_root(), 'data')) + +tests = [ + 'highlighter', + 'insertSorted', + 'jsParse', + 'markup', + 'params', + 'signalTracker', + 'url', + 'versionCompare', +] + +foreach test : tests + 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] ..." + 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 @@ + + + + + + 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 @@ + + + + + + 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 @@ + + + + + + diff --git a/tests/testcommon/border-image.png b/tests/testcommon/border-image.png new file mode 100644 index 0000000..e680020 Binary files /dev/null and b/tests/testcommon/border-image.png differ diff --git a/tests/testcommon/face-plain.png b/tests/testcommon/face-plain.png new file mode 100644 index 0000000..962d70f Binary files /dev/null and b/tests/testcommon/face-plain.png 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/highlighter.js b/tests/unit/highlighter.js new file mode 100644 index 0000000..d582d38 --- /dev/null +++ b/tests/unit/highlighter.js @@ -0,0 +1,106 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +// Test cases for SearchResult description match highlighter + +const JsUnit = imports.jsUnit; +const Pango = imports.gi.Pango; + +const Environment = imports.ui.environment; +Environment.init(); + +const Util = imports.misc.util; + +const tests = [ + { input: 'abc cba', + terms: null, + output: 'abc cba' }, + { input: 'abc cba', + terms: [], + output: 'abc cba' }, + { input: 'abc cba', + terms: [''], + output: 'abc cba' }, + { input: 'abc cba', + terms: ['a'], + output: 'abc cba' }, + { input: 'abc cba', + terms: ['a', 'a'], + output: 'abc cba' }, + { input: 'CaSe InSenSiTiVe', + terms: ['cas', 'sens'], + output: 'CaSe InSenSiTiVe' }, + { input: 'This contains the < character', + terms: null, + output: 'This contains the < character' }, + { input: 'Don\'t', + terms: ['t'], + output: 'Don't' }, + { input: 'Don\'t', + terms: ['n\'t'], + output: 'Don't' }, + { input: 'Don\'t', + terms: ['o', 't'], + output: 'Don't' }, + { input: 'salt&pepper', + terms: ['salt'], + output: 'salt&pepper' }, + { input: 'salt&pepper', + terms: ['salt', 'alt'], + output: 'salt&pepper' }, + { input: 'salt&pepper', + terms: ['pepper'], + output: 'salt&pepper' }, + { input: 'salt&pepper', + terms: ['salt', 'pepper'], + output: 'salt&pepper' }, + { input: 'salt&pepper', + terms: ['t', 'p'], + output: 'salt&pepper' }, + { input: 'salt&pepper', + terms: ['t', '&', 'p'], + output: 'salt&pepper' }, + { input: 'salt&pepper', + terms: ['e'], + output: 'salt&pepper' }, + { input: 'salt&pepper', + terms: ['&a', '&am', '&', '&'], + output: 'salt&pepper' }, + { input: '&&&&&', + terms: ['a'], + output: '&&&&&' }, + { input: '&;&;&;&;&;', + terms: ['a'], + output: '&;&;&;&;&;' }, + { input: '&;&;&;&;&;', + terms: [';'], + output: '&;&;&;&;&;' }, + { input: '&', + terms: ['a'], + output: '&amp;' } +]; + +try { + for (let i = 0; i < tests.length; i++) { + let highlighter = new Util.Highlighter(tests[i].terms); + let output = highlighter.highlight(tests[i].input); + + JsUnit.assertEquals(`Test ${i + 1} highlight ` + + `"${tests[i].terms}" in "${tests[i].input}"`, + output, tests[i].output); + + let parsed = false; + try { + Pango.parse_markup(output, -1, ''); + parsed = true; + } catch (e) {} + JsUnit.assertEquals(`Test ${i + 1} is valid markup`, true, parsed); + } +} catch (e) { + if (typeof(e.isJsUnitException) != 'undefined' + && e.isJsUnitException) + { + if (e.comment) + log(`Error in: ${e.comment}`); + } + throw e; +} 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('foo'); +assertEscapes('foo', '<b>foo</b>'); + +assertConverts('something foo'); +assertEscapes('something foo', 'something <i>foo</i>'); + +assertConverts('foo something'); +assertEscapes('foo something', '<u>foo</u> something'); + +assertConverts('bold italic and underlined'); +assertEscapes('bold italic and underlined', '<b>bold</b> <i>italic <u>and underlined</u></i>'); + +assertConverts('this & that'); +assertEscapes('this & that', 'this &amp; that'); + +assertConverts('this < that'); +assertEscapes('this < that', 'this &lt; that'); + +assertConverts('this < that > the other'); +assertEscapes('this < that > the other', 'this &lt; that &gt; the other'); + +assertConverts('this <that>'); +assertEscapes('this <that>', 'this &lt;<i>that</i>&gt;'); + +assertConverts('this > that'); +assertEscapes('this > that', '<b>this</b> > <i>that</i>'); + + + +// PARTIALLY CORRECT MARKUP +// correct bits are kept, incorrect bits are escaped + +// unrecognized entity +assertConverts('smile ☺!', 'smile &#9786;!'); +assertEscapes('smile ☺!', '<b>smile</b> &#9786;!'); + +// stray '&'; this is really a bug, but it's easier to do it this way +assertConverts('this & that', 'this & that'); +assertEscapes('this & that', '<b>this</b> & <i>that</i>'); + +// likewise with stray '<' +assertConverts('this < that', 'this < that'); +assertEscapes('this < that', 'this < that'); + +assertConverts('this < that', 'this < that'); +assertEscapes('this < that', '<b>this</b> < <i>that</i>'); + +assertConverts('this < that > the other', 'this < that > the other'); +assertEscapes('this < that > the other', 'this < that > the other'); + +assertConverts('this <that>', 'this <that>'); +assertEscapes('this <that>', 'this <<i>that</i>>'); + +// unknown tags +assertConverts('tag', '<unknown>tag</unknown>'); +assertEscapes('tag', '<unknown>tag</unknown>'); + +// make sure we check beyond the first letter +assertConverts('tag', '<bunknown>tag</bunknown>'); +assertEscapes('tag', '<bunknown>tag</bunknown>'); + +// with mix of good and bad, we keep the good and escape the bad +assertConverts('known and tag', 'known and <unknown>tag</unknown>'); +assertEscapes('known and tag', '<i>known</i> and <unknown>tag</unknown>'); + + + +// FULLY INCORRECT MARKUP +// (fall back to escaping the whole thing) + +// tags not matched up +assertConverts('incomplete', '<b>in<i>com</i>plete'); +assertEscapes('incomplete', '<b>in<i>com</i>plete'); + +assertConverts('incomplete', 'in<i>com</i>plete</b>'); +assertEscapes('incomplete', 'in<i>com</i>plete</b>'); + +// 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('good and bad', '<b>good</b> and <b style='bad'>bad</b>'); +assertEscapes('good and bad', '<b>good</b> and <b style='bad'>bad</b>'); + +// this is just syntactically invalid +assertConverts('unrecognized', '<b>unrecognized</b stuff>'); +assertEscapes('unrecognized', '<b>unrecognized</b stuff>'); + +// mismatched tags +assertConverts('mismatched', '<b>mismatched</i>'); +assertEscapes('mismatched', '<b>mismatched</i>'); + +assertConverts('mismatched/unknown', '<b>mismatched/unknown</bunknown>'); +assertEscapes('mismatched/unknown', '<b>mismatched/unknown</bunknown>'); 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/signalTracker.js b/tests/unit/signalTracker.js new file mode 100644 index 0000000..7943d0a --- /dev/null +++ b/tests/unit/signalTracker.js @@ -0,0 +1,115 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +// Test cases for version comparison + +const { GObject } = imports.gi; + +const JsUnit = imports.jsUnit; +const Signals = imports.misc.signals; + +const Environment = imports.ui.environment; +const { TransientSignalHolder, registerDestroyableType } = imports.misc.signalTracker; + +Environment.init(); + +const Destroyable = GObject.registerClass({ + Signals: { 'destroy': {} }, +}, class Destroyable extends GObject.Object {}); +registerDestroyableType(Destroyable); + +const GObjectEmitter = GObject.registerClass({ + Signals: { 'signal': {} }, +}, class GObjectEmitter extends Destroyable {}); + +const emitter1 = new Signals.EventEmitter(); +const emitter2 = new GObjectEmitter(); + +const tracked1 = new Destroyable(); +const tracked2 = {}; + +let count = 0; +const handler = () => count++; + +emitter1.connectObject('signal', handler, tracked1); +emitter2.connectObject('signal', handler, tracked1); + +emitter1.connectObject('signal', handler, tracked2); +emitter2.connectObject('signal', handler, tracked2); + +JsUnit.assertEquals(count, 0); + +emitter1.emit('signal'); +emitter2.emit('signal'); + +JsUnit.assertEquals(count, 4); + +tracked1.emit('destroy'); + +emitter1.emit('signal'); +emitter2.emit('signal'); + +JsUnit.assertEquals(count, 6); + +emitter1.disconnectObject(tracked2); +emitter2.emit('destroy'); + +emitter1.emit('signal'); +emitter2.emit('signal'); + +JsUnit.assertEquals(count, 6); + +emitter1.connectObject( + 'signal', handler, + 'signal', handler, GObject.ConnectFlags.AFTER, + tracked1); +emitter2.connectObject( + 'signal', handler, + 'signal', handler, GObject.ConnectFlags.AFTER, + tracked1); + +emitter1.emit('signal'); +emitter2.emit('signal'); + +JsUnit.assertEquals(count, 10); + +tracked1.emit('destroy'); +emitter1.emit('signal'); +emitter2.emit('signal'); + +JsUnit.assertEquals(count, 10); + +emitter1.connectObject('signal', handler, tracked1); +emitter2.connectObject('signal', handler, tracked1); + +transientHolder = new TransientSignalHolder(tracked1); + +emitter1.connectObject('signal', handler, transientHolder); +emitter2.connectObject('signal', handler, transientHolder); + +emitter1.emit('signal'); +emitter2.emit('signal'); + +JsUnit.assertEquals(count, 14); + +transientHolder.destroy(); + +emitter1.emit('signal'); +emitter2.emit('signal'); + +JsUnit.assertEquals(count, 16); + +transientHolder = new TransientSignalHolder(tracked1); + +emitter1.connectObject('signal', handler, transientHolder); +emitter2.connectObject('signal', handler, transientHolder); + +emitter1.emit('signal'); +emitter2.emit('signal'); + +JsUnit.assertEquals(count, 20); + +tracked1.emit('destroy'); +emitter1.emit('signal'); +emitter2.emit('signal'); + +JsUnit.assertEquals(count, 20); 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); + } +} diff --git a/tests/unit/versionCompare.js b/tests/unit/versionCompare.js new file mode 100644 index 0000000..1997a6c --- /dev/null +++ b/tests/unit/versionCompare.js @@ -0,0 +1,52 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +// Test cases for version comparison + +const JsUnit = imports.jsUnit; + +const Environment = imports.ui.environment; +Environment.init(); + +const Util = imports.misc.util; + +const tests = [ + { v1: '40', + v2: '40', + res: 0 }, + { v1: '40', + v2: '42', + res: -1 }, + { v1: '42', + v2: '40', + res: 1 }, + { v1: '3.38.0', + v2: '40', + res: -1 }, + { v1: '40', + v2: '3.38.0', + res: 1 }, + { v1: '40', + v2: '3.38.0', + res: 1 }, + { v1: '40.alpha.1.1', + v2: '40', + res: -1 }, + { v1: '40', + v2: '40.alpha.1.1', + res: 1 }, + { v1: '40.beta', + v2: '40', + res: -1 }, + { v1: '40.1', + v2: '40', + res: 1 }, + { v1: '', + v2: '40.alpha', + res: -1 }, +]; + +for (let i = 0; i < tests.length; i++) { + name = 'Test #' + i + ' v1: ' + tests[i].v1 + ', v2: ' + tests[i].v2; + print(name); + JsUnit.assertEquals(name, Util.GNOMEversionCompare (tests[i].v1, tests[i].v2), tests[i].res); +} -- cgit v1.2.3