diff --git a/bookshop/app/vue/index.html b/bookshop/app/vue/index.html
index c8e23209..e18eabe7 100644
--- a/bookshop/app/vue/index.html
+++ b/bookshop/app/vue/index.html
@@ -44,11 +44,11 @@
{{ document.title }}
{{ book.title }}
diff --git a/package.json b/package.json
index 9c5c6c34..0bf966db 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,8 @@
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"chai-subset": "^1.6.0",
+ "@cucumber/cucumber": "^7.0.0",
+ "selenium-webdriver": "^4.0.0-beta.1",
"sqlite3": "5.0.0"
},
"scripts": {
diff --git a/test/cds.ql.test.js b/test/cds.ql.test.js
index a8bfa7be..dfade6b0 100644
--- a/test/cds.ql.test.js
+++ b/test/cds.ql.test.js
@@ -365,7 +365,7 @@ describe('cds.ql → cqn', () => {
},
})
- expect(
+ if (!is_v2) expect(
SELECT.from(Foo).where(`x=`, 1, `or y.z is null and (a>`, 2, `or b=`, 3, `)`)
).to.eql(CQL`SELECT from Foo where x=1 or y.z is null and (a>2 or b=3)`)
diff --git a/test/features/bookshop.feature b/test/features/bookshop.feature
new file mode 100644
index 00000000..ab3288b7
--- /dev/null
+++ b/test/features/bookshop.feature
@@ -0,0 +1,34 @@
+Feature: List Books using Vue.js UI
+
+ Scenario: Launch cds server for bookshop
+ When we run the 'bookshop' server
+ And wait for 1s
+ Then it should listen at 'http://localhost:4004'
+
+ Scenario: Display Books List
+ When we open page '/vue/index.html'
+ And wait for 1s
+ Then it should list these rows in table 'books':
+ | Wuthering Heights | Emily Brontë |
+ | Jane Eyre | Charlotte Brontë |
+ | The Raven | Edgar Allen Poe |
+ | Eleonora | Edgar Allen Poe |
+ | Catweazle | Richard Carpenter |
+
+ Scenario: Select a Book
+ When we click on the 1st row in table 'books'
+ Then it shows '12' in 'stock'
+
+ Scenario: Order One Book
+ When we click on button 'Order:'
+ Then it succeeds with 'ordered 1 item(s)'
+
+ Scenario: Order Four Books
+ When we enter '4' into 'amount'
+ And we click on button 'Order:'
+ Then it succeeds with 'ordered 4 item(s)'
+
+ Scenario: Order Amount Exceeding Stock
+ When we enter '9' into 'amount'
+ And we click on button 'Order:'
+ Then it fails with '9 exceeds stock'
diff --git a/test/features/step_definitions/bookshop_steps.js b/test/features/step_definitions/bookshop_steps.js
new file mode 100644
index 00000000..131eb9a6
--- /dev/null
+++ b/test/features/step_definitions/bookshop_steps.js
@@ -0,0 +1,82 @@
+const { Given, When, Then, AfterAll } = require('@cucumber/cucumber')
+const { Builder, By } = require('selenium-webdriver')
+const browser = (new Builder).forBrowser('safari').build()
+const cds = require ('@sap/cds/lib')
+
+process.env.cds_requires_auth_strategy = 'dummy'
+
+let axios = require('axios').default
+let {display} = browser
+
+When('we run the {string} server', project => cds.exec('watch', project))
+Then('it should listen at {string}', baseURL => {
+ axios = axios.create({ baseURL })
+ display = url => browser.get (baseURL+url)
+ return axios.head()
+})
+Then('terminate the server', ()=> process.exit())
+
+
+
+When(/wait for (\d+)\s*(\w+)/, {timeout:60*1000}, (delay, unit, done) => {
+ const factor = {
+ ms: 1,
+ s: 1000, sec: 1000, second: 1000, seconds: 1000,
+ m: 60*1000, min: 60*1000, minute: 60*1000, minutes: 60*1000,
+ h: 60*60*1000, hr: 60*60*1000, hour: 60*60*1000, hours: 60*60*1000,
+ }[unit]
+ if (!factor) throw `Unknown duration unit: ${unit}`
+ setTimeout (done, delay * factor)
+})
+
+AfterAll(()=> setTimeout (process.exit, 111))
+
+
+
+
+
+When('we open page {string}', page => display(page))
+
+Then('it should list these rows in table {string}:', async (id,data) => {
+ let rows = await browser.findElements(By.css(`#${id} tr`)); rows.shift()
+ await Promise.all (data.rawTable.map (async (row,i)=>{
+ const tr = await rows[i].getText()
+ for (let each of row) if (!tr.match(each)) throw `Didn't find '${each}' in web page as expected`
+ }))
+})
+
+When(/we click on the (\d+)(?:st|nd|rd|th) row in table '(\w+)'/, async (row, id) => {
+ let rows = await browser.findElements(By.css(`#${id} tr`))
+ let td = await rows[row].findElement(By.css('td'))
+ return td.click()
+})
+
+When('we enter {string} into {string}', async (value,id) => {
+ const field = await browser.findElement(By.css(`input#${id}`))
+ return field.sendKeys('\b\b\b\b\b\b',value)
+})
+
+When('we click on button {string}', async (text) => {
+ const button = await browser.findElement(By.css(`input[value='${text}']`))
+ return button.click()
+})
+
+Then('it succeeds with {string}', async message => {
+ const element = await browser.wait (browser.findElement(By.css(`span.succeeded`)))
+ return (await element.getText()).includes(message)
+})
+
+Then('it fails with {string}', async message => {
+ const element = await browser.wait (browser.findElement(By.css(`span.failed`)))
+ return (await element.getText()).includes(message)
+})
+
+Then('it shows {string} in {string}', async (message,id) => {
+ const element = await browser.wait (browser.findElement(By.id(id)))
+ return (await element.getText()).includes(message)
+})
+
+Given('we login as {string}, {string}', async (username, password) => {
+ const alert = await browser.switchTo().alert()
+ return alert.authenticateAs(username, password)
+})