on xenial help sign in
Add initial Card.qml and CardHeader.qml, with tests.
3bbd428Michał Sawicz
Dash/Card.qmlnew file+65
@@ -0,0 +1,65 @@
1+import QtQuick 2.0
2+import Ubuntu.Components 0.1
3+
4+Item {
5+ id: root
6+ property var template
7+ property var components
8+ property var cardData
9+
10+ width: {
11+ if (template !== undefined) {
12+ switch (template['card-size']) {
13+ case "small": return units.gu(12);
14+ case "large": return units.gu(38);
15+ }
16+ }
17+ return units.gu(18.5);
18+ }
19+ height: childrenRect.height
20+
21+ UbuntuShape {
22+ id: artShape
23+ objectName: "artShape"
24+ width: image.fillMode === Image.PreserveAspectCrop || aspect < image.aspect ? image.width : height * image.aspect
25+ height: image.fillMode === Image.PreserveAspectCrop || aspect > image.aspect ? image.height : width / image.aspect
26+ anchors.horizontalCenter: parent.horizontalCenter
27+
28+ property real aspect: components !== undefined ? components["art"]["aspect-ratio"] : 1
29+
30+ image: Image {
31+ width: root.width
32+ height: width / artShape.aspect
33+ objectName: "artImage"
34+ source: cardData && cardData["art"] || ""
35+ //sourceSize.width: width > height ? width : 0
36+ //sourceSize.height: height > width ? height : -1
37+ fillMode: components["art"]["fill-mode"] == "fit" ? Image.PreserveAspectFit: Image.PreserveAspectCrop
38+
39+ property real aspect: implicitWidth / implicitHeight
40+ }
41+ }
42+
43+ CardHeader {
44+ id: header
45+ objectName: "cardHeader"
46+ anchors {
47+ top: artShape.bottom
48+ left: parent.left
49+ right: parent.right
50+ }
51+
52+ mascot: cardData && cardData["mascot"] || ""
53+ title: cardData && cardData["title"] || ""
54+ subtitle: cardData && cardData["subtitle"] || ""
55+ }
56+
57+ Label {
58+ objectName: "summaryLabel"
59+ anchors { top: header.bottom; left: parent.left; right: parent.right }
60+ wrapMode: Text.Wrap
61+ maximumLineCount: 5
62+ elide: Text.ElideRight
63+ text: cardData && cardData["summary"] || ""
64+ }
65+}
Dash/CardHeader.qmlnew file+94
@@ -0,0 +1,94 @@
1+import QtQuick 2.0
2+import Ubuntu.Components 0.1
3+
4+Item {
5+ id: root
6+ property alias mascot: avatarImage.source
7+ property alias title: titleLabel.text
8+ property alias subtitle: subtitleLabel.text
9+ property alias price: priceLabel.text
10+ property alias oldPrice: oldPriceLabel.text
11+ property alias altPrice: altPriceLabel.text
12+
13+ visible: avatarImage.status === Image.Ready || title || price
14+ height: row.height > 0 ? row.height + row.spacing * 2 : 0
15+
16+ Row {
17+ id: row
18+
19+ anchors { top: parent.top; left: parent.left; right: parent.right; margins: spacing }
20+ spacing: units.gu(1)
21+
22+ UbuntuShape {
23+ id: avatarShape
24+ objectName: "avatarShape"
25+
26+ width: units.gu(8)
27+ height: units.gu(8)
28+ visible: image.status === Image.Ready
29+
30+ image: Image {
31+ id: avatarImage
32+ sourceSize { width: avatarShape.width; height: avatarShape.height }
33+ }
34+ }
35+
36+ Column {
37+ width: parent.width - x - row.spacing
38+
39+ Label {
40+ id: titleLabel
41+ anchors { left: parent.left; right: parent.right }
42+ elide: Text.ElideRight
43+ objectName: "titleLabel"
44+ font.weight: Font.DemiBold
45+ wrapMode: Text.Wrap
46+ maximumLineCount: 2
47+ }
48+
49+ Label {
50+ id: subtitleLabel
51+ anchors { left: parent.left; right: parent.right }
52+ elide: Text.ElideRight
53+ visible: titleLabel.text && text
54+ }
55+
56+ Row {
57+ id: prices
58+ objectName: "prices"
59+ anchors { left: parent.left; right: parent.right }
60+
61+ property int labels: {
62+ var labels = 1; // price always visible
63+ if (oldPriceLabel.text !== "") labels += 1;
64+ if (altPriceLabel.text !== "") labels += 1;
65+ return labels;
66+ }
67+ property real labelWidth: width / labels
68+
69+ Label {
70+ id: priceLabel
71+ width: parent.labelWidth;
72+ elide: Text.ElideRight
73+ font.weight: Font.DemiBold
74+ color: Theme.palette.selected.foreground
75+ }
76+
77+ Label {
78+ id: oldPriceLabel
79+ objectName: "oldPriceLabel"
80+ width: parent.labelWidth;
81+ elide: Text.ElideRight
82+ horizontalAlignment: parent.labels === 3 ? Text.AlignHCenter : Text.AlignRight
83+ }
84+
85+ Label {
86+ id: altPriceLabel
87+ width: parent.labelWidth;
88+ elide: Text.ElideRight
89+ horizontalAlignment: Text.AlignRight
90+ }
91+ }
92+ }
93+ }
94+}
tests/qmltests/CMakeLists.txt+2
@@ -43,6 +43,8 @@ add_qml_test(Dash DashBar IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins ${CMAKE_CURRE
43add_qml_test(Dash DashContent IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins ${qmltest_DEFAULT_IMPORT_PATHS})
43add_qml_test(Dash DashContent IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins ${qmltest_DEFAULT_IMPORT_PATHS})
44add_qml_test(Dash DashPreview)
44add_qml_test(Dash DashPreview)
45add_qml_test(Dash GenericPreview)
45add_qml_test(Dash GenericPreview)
46+add_qml_test(Dash Card)
47+add_qml_test(Dash CardHeader)
46add_qml_test(Dash GenericScopeView IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins ${qmltest_DEFAULT_IMPORT_PATHS})
48add_qml_test(Dash GenericScopeView IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins ${qmltest_DEFAULT_IMPORT_PATHS})
47add_qml_test(Dash FilterGrids IMPORT_PATHS ${qmltest_DEFAULT_IMPORT_PATHS} ${CMAKE_BINARY_DIR}/plugins ${CMAKE_CURRENT_SOURCE_DIR}/plugins
49add_qml_test(Dash FilterGrids IMPORT_PATHS ${qmltest_DEFAULT_IMPORT_PATHS} ${CMAKE_BINARY_DIR}/plugins ${CMAKE_CURRENT_SOURCE_DIR}/plugins
48 ${CMAKE_BINARY_DIR}/tests/mocks)
50 ${CMAKE_BINARY_DIR}/tests/mocks)
tests/qmltests/Dash/CardHelpers.jsnew file+59
@@ -0,0 +1,59 @@
1+.pragma library
2+
3+var components = ["title", "art", "subtitle", "mascot", "emblem", "old-price", "price", "alt-price", "rating", "alt-rating", "summary"]
4+
5+function tryParse(json, errorLabel) {
6+ var o = undefined;
7+ if (errorLabel !== undefined) {
8+ errorLabel.text = "";
9+ }
10+ try {
11+ o = JSON.parse(json)
12+ } catch(err) {
13+ if (errorLabel !== undefined) {
14+ errorLabel.text = err + "";
15+ } else {
16+ console.debug(err);
17+ }
18+ }
19+ return o;
20+}
21+
22+function mapData(json, layout, errorLabel) {
23+ var o = tryParse(json, errorLabel);
24+ var d = undefined;
25+
26+ if (o !== undefined) {
27+ d = Object();
28+ for (var k in components) {
29+ try {
30+ if (typeof layout[components[k]] == "object") {
31+ d[components[k]] = o[layout[components[k]]['field']];
32+ } else {
33+ d[components[k]] = o[layout[components[k]]];
34+ }
35+ } catch(err) {
36+ d[components[k]] = undefined;
37+ }
38+ }
39+ }
40+ return d;
41+}
42+
43+function update(object, overrides) {
44+ for (var k in overrides) {
45+ if (typeof object[k] == "string" && typeof overrides[k] == "object") {
46+ if (!overrides[k].hasOwnProperty('field')) overrides[k]['field'] = object[k];
47+ }
48+ if (object[k] === null) {
49+ object[k] = overrides[k];
50+ } else if (typeof object[k] == "object" && typeof overrides[k] == "string") {
51+ object[k]['field'] = overrides[k];
52+ } else if (typeof object[k] == "object") {
53+ update(object[k], overrides[k]);
54+ } else {
55+ object[k] = overrides[k];
56+ }
57+ }
58+ return object;
59+}
tests/qmltests/Dash/artwork/avatar@12.pngnew file

Binary file not shown.

tests/qmltests/Dash/artwork/music-player-design.pngnew file

Binary file not shown.

tests/qmltests/Dash/tst_Card.qmlnew file+293
@@ -0,0 +1,293 @@
1+import QtQuick 2.0
2+import QtTest 1.0
3+import Ubuntu.Components 0.1
4+import "../../../Dash"
5+import "CardHelpers.js" as Helpers
6+
7+Rectangle {
8+ id: root
9+ width: units.gu(80)
10+ height: units.gu(72)
11+ color: "#88FFFFFF"
12+
13+ property string defaultLayout: '
14+ {
15+ "schema-version": 1,
16+ "template": {
17+ "category-layout": "grid",
18+ "card-layout": "vertical",
19+ "card-size": "medium",
20+ "overlay-mode": null,
21+ "collapsed-rows": 2
22+ },
23+ "components": {
24+ "title": null,
25+ "art": {
26+ "aspect-ratio": 1.0,
27+ "fill-mode": "crop"
28+ },
29+ "subtitle": null,
30+ "mascot": null,
31+ "emblem": null,
32+ "old-price": null,
33+ "price": null,
34+ "alt-price": null,
35+ "rating": {
36+ "type": "stars",
37+ "range": [0, 5],
38+ "full": "image://theme/rating-star-full",
39+ "half": "image://theme/rating-star-half",
40+ "empty": "image://theme/rating-star-empty"
41+ },
42+ "alt-rating": null,
43+ "summary": null
44+ },
45+ "resources": {}
46+ }'
47+
48+ property string cardData: '
49+ {
50+ "art": "../tests/qmltests/Dash/artwork/music-player-design.png",
51+ "mascot": "../tests/qmltests/Dash/artwork/avatar.png",
52+ "title": "foo",
53+ "subtitle": "bar",
54+ "summary": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
55+ }'
56+
57+ property string fullMapping: '
58+ {
59+ "title": "title",
60+ "art": "art",
61+ "subtitle": "subtitle",
62+ "mascot": "mascot",
63+ "summary": "summary"
64+ }'
65+
66+ property var cardsModel: [
67+ {
68+ "name": "Art, header, summary - vertical",
69+ "layout": { "components": JSON.parse(fullMapping) }
70+ },
71+ {
72+ "name": "Art, header, summary - vertical, small",
73+ "layout": { "template": { "card-size": "small" }, "components": JSON.parse(fullMapping) }
74+ },
75+ {
76+ "name": "Art, header, summary - vertical, large",
77+ "layout": { "template": { "card-size": "large" }, "components": JSON.parse(fullMapping) }
78+ },
79+ {
80+ "name": "Art, header, summary - vertical, wide",
81+ "layout": { "components": Helpers.update(JSON.parse(root.fullMapping), { "art": { "aspect-ratio": 2 } }) }
82+ },
83+ {
84+ "name": "Art, title - vertical, fitted",
85+ "layout": { "components": Helpers.update(JSON.parse(root.fullMapping), { "art": { "fill-mode": "fit" } }) }
86+ }
87+ ]
88+
89+ Card {
90+ id: card
91+ anchors { top: parent.top; left: parent.left; margins: units.gu(1) }
92+
93+ template: Helpers.update(JSON.parse(root.defaultLayout), Helpers.tryParse(layoutArea.text, layoutError))['template'];
94+ components: Helpers.update(JSON.parse(root.defaultLayout), Helpers.tryParse(layoutArea.text, layoutError))['components'];
95+ cardData: Helpers.mapData(dataArea.text, components, dataError)
96+ }
97+
98+ Rectangle {
99+ anchors { top: parent.top; bottom: parent.bottom; right: parent.right}
100+ width: units.gu(40)
101+ color: "lightgrey"
102+
103+ Column {
104+ anchors { fill: parent; margins: units.gu(1) }
105+ spacing: units.gu(1)
106+
107+ OptionSelector {
108+ id: selector
109+ model: cardsModel
110+ delegate: OptionSelectorDelegate { text: modelData.name }
111+ onSelectedIndexChanged: updateAreas()
112+ Component.onCompleted: updateAreas()
113+
114+ function updateAreas() {
115+ var element = cardsModel[selectedIndex];
116+ if (element) {
117+ layoutArea.text = JSON.stringify(element.layout, undefined, 2) || "{}";
118+ // FIXME: don't overwrite data
119+ var data = JSON.parse(root.cardData);
120+ Helpers.update(data, element.data);
121+ dataArea.text = JSON.stringify(data, undefined, 2) || "{}";
122+ } else {
123+ layoutArea.text = "";
124+ dataArea.text = "";
125+ }
126+
127+ }
128+ }
129+
130+ TextArea {
131+ id: layoutArea
132+ anchors { left: parent.left; right: parent.right }
133+ height: units.gu(25)
134+ }
135+
136+ Label {
137+ id: layoutError
138+ anchors { left: parent.left; right: parent.right }
139+ height: units.gu(4)
140+ color: "orange"
141+ }
142+
143+ TextArea {
144+ id: dataArea
145+ anchors { left: parent.left; right: parent.right }
146+ height: units.gu(25)
147+ }
148+
149+ Label {
150+ id: dataError
151+ anchors { left: parent.left; right: parent.right }
152+ height: units.gu(4)
153+ color: "orange"
154+ }
155+ }
156+ }
157+
158+ TestCase {
159+ id: testCase
160+
161+ when: windowShown
162+
163+ property Item header: findChild(card, "cardHeader")
164+ property Item art: findChild(card, "artShape")
165+ property Item artImage: findChild(card, "artImage")
166+ property Item summary: findChild(card, "summaryLabel")
167+
168+ // Find an object with the given name in the children tree of "obj"
169+ function findChild(obj,objectName) {
170+ var childs = new Array(0);
171+ childs.push(obj)
172+ while (childs.length > 0) {
173+ if (childs[0].objectName == objectName) {
174+ return childs[0]
175+ }
176+ for (var i in childs[0].children) {
177+ childs.push(childs[0].children[i])
178+ }
179+ childs.splice(0, 1);
180+ }
181+ return null;
182+ }
183+
184+ function initTestCase() {
185+ verify(testCase.header !== undefined, "Couldn't find header object.");
186+ }
187+
188+ function cleanup() {
189+ selector.selectedIndex = -1;
190+ }
191+
192+
193+ function test_header_binding_data() {
194+ return [
195+ { tag: "Mascot", property: "mascot", value: Qt.resolvedUrl("artwork/avatar.png"), index: 0 },
196+ { tag: "Title", property: "title", value: "foo", index: 0 },
197+ { tag: "Subtitle", property: "subtitle", value: "bar", index: 0 },
198+ ];
199+ }
200+
201+ function test_header_binding(data) {
202+ selector.selectedIndex = data.index;
203+ tryCompare(testCase.header, data.property, data.value);
204+ }
205+
206+ function test_card_binding_data() {
207+ return [
208+ { tag: "Art", object: artImage, property: "source", value: Qt.resolvedUrl("artwork/music-player-design.png"), index: 0 },
209+ { tag: "Summary", object: summary, property: "text", field: "summary", index: 0 },
210+ { tag: "Fit", object: art, fill: Image.PreserveAspectFit, index: 4 },
211+ ];
212+ }
213+
214+ function test_card_binding(data) {
215+ selector.selectedIndex = data.index;
216+
217+ if (data.hasOwnProperty('value')) {
218+ tryCompare(data.object, data.property, data.value);
219+ }
220+
221+ if (data.hasOwnProperty('field')) {
222+ tryCompare(data.object, data.property, card.cardData[data.field]);
223+ }
224+ }
225+
226+ function test_card_size_data() {
227+ return [
228+ { tag: "Medium", width: units.gu(18.5), index: 0 },
229+ { tag: "Small", width: units.gu(12), size: "small", index: 0 },
230+ { tag: "Large", width: units.gu(38), size: "large", index: 0 },
231+ { tag: "Wide", width: units.gu(18.5), aspect: 0.5, index: 0 },
232+ ]
233+ }
234+
235+ function test_card_size(data) {
236+ selector.selectedIndex = data.index;
237+
238+ if (data.hasOwnProperty("size")) {
239+ card.template['card-size'] = data.size;
240+ card.templateChanged();
241+ }
242+
243+ if (data.hasOwnProperty("aspect")) {
244+ card.components['art']['aspect-ratio'] = data.aspect;
245+ card.componentsChanged();
246+ }
247+
248+ if (data.hasOwnProperty("width")) {
249+ tryCompare(card, "width", data.width);
250+ }
251+
252+ if (data.hasOwnProperty("height")) {
253+ tryCompare(card, "height", data.height);
254+ }
255+ }
256+
257+ function test_art_size_data() {
258+ return [
259+ { tag: "Medium", width: units.gu(18.5), fill: Image.PreserveAspectCrop, index: 0 },
260+ { tag: "Small", width: units.gu(12), index: 1 },
261+ { tag: "Large", width: units.gu(38), index: 2 },
262+ { tag: "Wide", height: units.gu(9.25), index: 3 },
263+ { tag: "Fit", height: units.gu(18.5), width: units.gu(9.25), index: 4 },
264+ ]
265+ }
266+
267+ function test_art_size(data) {
268+ selector.selectedIndex = data.index;
269+
270+ if (data.hasOwnProperty("size")) {
271+ card.template['card-size'] = data.size;
272+ card.templateChanged();
273+ }
274+
275+ if (data.hasOwnProperty("aspect")) {
276+ card.components['art']['aspect-ratio'] = data.aspect;
277+ card.componentsChanged();
278+ }
279+
280+ if (data.hasOwnProperty("width")) {
281+ tryCompare(art, "width", data.width);
282+ }
283+
284+ if (data.hasOwnProperty("height")) {
285+ tryCompare(art, "height", data.height);
286+ }
287+
288+ if (data.hasOwnProperty("fill")) {
289+ tryCompare(artImage, "fillMode", data.fill);
290+ }
291+ }
292+ }
293+}
tests/qmltests/Dash/tst_CardHeader.qmlnew file+120
@@ -0,0 +1,120 @@
1+import QtQuick 2.0
2+import QtTest 1.0
3+import Ubuntu.Components 0.1
4+import "../../../Dash"
5+
6+
7+Rectangle {
8+ width: units.gu(40)
9+ height: units.gu(72)
10+ color: "lightgrey"
11+
12+ CardHeader {
13+ id: cardHeader
14+ anchors { left: parent.left; right: parent.right }
15+ }
16+
17+ Rectangle {
18+ anchors.fill: cardHeader
19+ color: "lightblue"
20+ opacity: 0.5
21+ }
22+
23+ TestCase {
24+ id: testCase
25+
26+ when: windowShown
27+
28+ // Find an object with the given name in the children tree of "obj"
29+ function findChild(obj,objectName) {
30+ var childs = new Array(0);
31+ childs.push(obj)
32+ while (childs.length > 0) {
33+ if (childs[0].objectName == objectName) {
34+ return childs[0]
35+ }
36+ for (var i in childs[0].children) {
37+ childs.push(childs[0].children[i])
38+ }
39+ childs.splice(0, 1);
40+ }
41+ return null;
42+ }
43+
44+ property Item avatar: findChild(cardHeader, "avatarShape")
45+ property Item titleLabel: findChild(cardHeader, "titleLabel")
46+ property Item subtitleLabel: findChild(cardHeader, "subtitleLabel")
47+ property Item prices: findChild(cardHeader, "prices")
48+ property Item oldPriceLabel: findChild(cardHeader, "oldPriceLabel")
49+
50+ function initTestCase() {
51+ verify(testCase.avatar !== undefined, "Couldn't find avatar object.");
52+ verify(testCase.titleLabel !== undefined, "Couldn't find titleLabel object.");
53+ verify(testCase.subtitleLabel !== undefined, "Couldn't find subtitleLabel object.");
54+ verify(testCase.prices !== undefined, "Couldn't find prices object.");
55+ verify(testCase.oldPriceLabel !== undefined, "Couldn't find oldPriceLabel object.");
56+ }
57+
58+ function cleanup() {
59+ cardHeader.mascot = "";
60+ cardHeader.title = "";
61+ cardHeader.subtitle = "";
62+ cardHeader.price = "";
63+ cardHeader.oldPrice = "";
64+ cardHeader.altPrice = "";
65+ }
66+
67+ function test_avatar_data() {
68+ return [
69+ { tag: "Empty", source: "", visible: false },
70+ { tag: "Invalid", source: "bad_path", visible: false },
71+ { tag: "Valid", source: "artwork/avatar.png", visible: true },
72+ ]
73+ }
74+
75+ function test_avatar(data) {
76+ cardHeader.mascot = data.source;
77+ tryCompare(testCase.avatar, "visible", data.visible);
78+ }
79+
80+ function test_labels_data() {
81+ return [
82+ { tag: "Empty", visible: false },
83+ { tag: "Title only", title: "Foo", visible: true },
84+ { tag: "Subtitle only", subtitle: "Bar", visible: false },
85+ { tag: "Both", title: "Foo", subtitle: "Bar", visible: true }
86+ ]
87+ }
88+
89+ function test_labels(data) {
90+ cardHeader.title = data.title !== undefined ? data.title : "";
91+ cardHeader.subtitle = data.subtitle !== undefined ? data.subtitle : "";
92+ tryCompare(cardHeader, "visible", data.visible);
93+ if (data.maxLineCount != undefined) {
94+ compare(testCase.titleLabel.maximumLineCount, data.maxLineCount, "titleLabel maximumLineCount should be %1".arg(data.maxLineCount));
95+ }
96+ }
97+
98+ function test_prices_data() {
99+ return [
100+ { tag: "Main", main: "$1.25", visible: true },
101+ { tag: "Alt", alt: "€1.00", visible: false },
102+ { tag: "Old", old: "€2.00", visible: false },
103+ { tag: "Main and Alt", main: "$1.25", alt: "€1.00", visible: true },
104+ { tag: "Main and Old", main: "$1.25", old: "$2.00", visible: true, oldAlign: Text.AlignRight },
105+ { tag: "Alt and Old", alt: "€1.00", old: "$2.00", visible: false },
106+ { tag: "All", main: "$1.25", alt: "€1.00", old: "$2.00", visible: true, oldAlign: Text.AlignHCenter }
107+ ]
108+ }
109+
110+ function test_prices(data) {
111+ cardHeader.price = data.main !== undefined ? data.main : "";
112+ cardHeader.oldPrice = data.old !== undefined ? data.old : "";
113+ cardHeader.altPrice = data.alt !== undefined ? data.alt : "";
114+ tryCompare(cardHeader, "visible", data.visible);
115+ if (data.oldAlign !== undefined) {
116+ compare(testCase.oldPriceLabel.horizontalAlignment, data.oldAlign, "Old price label is aligned wrong.")
117+ }
118+ }
119+ }
120+}