diff options
Diffstat (limited to '')
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 Binary files differnew file mode 100644 index 0000000..e680020 --- /dev/null +++ b/tests/testcommon/border-image.png diff --git a/tests/testcommon/face-plain.png b/tests/testcommon/face-plain.png Binary files differnew file mode 100644 index 0000000..962d70f --- /dev/null +++ b/tests/testcommon/face-plain.png 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>', '<b>foo</b>'); + +assertConverts('something <i>foo</i>'); +assertEscapes('something <i>foo</i>', 'something <i>foo</i>'); + +assertConverts('<u>foo</u> something'); +assertEscapes('<u>foo</u> something', '<u>foo</u> something'); + +assertConverts('<b>bold</b> <i>italic <u>and underlined</u></i>'); +assertEscapes('<b>bold</b> <i>italic <u>and underlined</u></i>', '<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 <<i>that</i>>'); +assertEscapes('this <<i>that</i>>', 'this &lt;<i>that</i>&gt;'); + +assertConverts('<b>this</b> > <i>that</i>'); +assertEscapes('<b>this</b> > <i>that</i>', '<b>this</b> > <i>that</i>'); + + + +// PARTIALLY CORRECT MARKUP +// correct bits are kept, incorrect bits are escaped + +// unrecognized entity +assertConverts('<b>smile</b> ☺!', '<b>smile</b> &#9786;!'); +assertEscapes('<b>smile</b> ☺!', '<b>smile</b> &#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> & <i>that</i>'); +assertEscapes('<b>this</b> & <i>that</i>', '<b>this</b> & <i>that</i>'); + +// likewise with stray '<' +assertConverts('this < that', 'this < that'); +assertEscapes('this < that', 'this < that'); + +assertConverts('<b>this</b> < <i>that</i>', '<b>this</b> < <i>that</i>'); +assertEscapes('<b>this</b> < <i>that</i>', '<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 <<i>that</i>>', 'this <<i>that</i>>'); +assertEscapes('this <<i>that</i>>', 'this <<i>that</i>>'); + +// unknown tags +assertConverts('<unknown>tag</unknown>', '<unknown>tag</unknown>'); +assertEscapes('<unknown>tag</unknown>', '<unknown>tag</unknown>'); + +// make sure we check beyond the first letter +assertConverts('<bunknown>tag</bunknown>', '<bunknown>tag</bunknown>'); +assertEscapes('<bunknown>tag</bunknown>', '<bunknown>tag</bunknown>'); + +// 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 <unknown>tag</unknown>'); +assertEscapes('<i>known</i> and <unknown>tag</unknown>', '<i>known</i> and <unknown>tag</unknown>'); + + + +// FULLY INCORRECT MARKUP +// (fall back to escaping the whole thing) + +// tags not matched up +assertConverts('<b>in<i>com</i>plete', '<b>in<i>com</i>plete'); +assertEscapes('<b>in<i>com</i>plete', '<b>in<i>com</i>plete'); + +assertConverts('in<i>com</i>plete</b>', 'in<i>com</i>plete</b>'); +assertEscapes('in<i>com</i>plete</b>', '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('<b>good</b> and <b style=\'bad\'>bad</b>', '<b>good</b> and <b style='bad'>bad</b>'); +assertEscapes('<b>good</b> and <b style=\'bad\'>bad</b>', '<b>good</b> and <b style='bad'>bad</b>'); + +// this is just syntactically invalid +assertConverts('<b>unrecognized</b stuff>', '<b>unrecognized</b stuff>'); +assertEscapes('<b>unrecognized</b stuff>', '<b>unrecognized</b stuff>'); + +// mismatched tags +assertConverts('<b>mismatched</i>', '<b>mismatched</i>'); +assertEscapes('<b>mismatched</i>', '<b>mismatched</i>'); + +assertConverts('<b>mismatched/unknown</bunknown>', '<b>mismatched/unknown</bunknown>'); +assertEscapes('<b>mismatched/unknown</bunknown>', '<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/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); + } +} |