mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo-client.git
synced 2026-09-21 16:52:02 -07:00
Implement New Post Page
Adds a New Post page for composing and broadcasting Memo posts: - src/services/new-post.js: testable controller (input, character counter counting down from the memo limit, validation/length errors, navigation to the recent feed after a successful post). - src/components/app-body/new-post: React view with a message textarea, post button, and remaining-character counter. - Route /posts/new and a navigation menu link. - Extend acceptance step handlers to cover both specs/post-memo.feature and specs/memo-new.feature wording; add unit tests for the controller. - Drop redundant 'use strict' in compiled modules to fix the CI build warning. By coder.
This commit is contained in:
+87
-32
@@ -1,18 +1,23 @@
|
||||
/*
|
||||
Project step handlers for the psf-memo-client acceptance pipeline.
|
||||
|
||||
These handlers connect Gherkin step text to the real project behavior in
|
||||
src/services/memo-post.js, driving it through small injected adapters (a fake
|
||||
wallet and a fake feed) so the acceptance run is deterministic and offline.
|
||||
These handlers connect Gherkin step text to real project behavior
|
||||
(src/services/memo-post.js and src/services/new-post.js), driving them
|
||||
through small injected adapters (a fake wallet, a fake feed, and a fake
|
||||
navigator) so the acceptance run is deterministic and offline.
|
||||
|
||||
Regex matching with placeholder-name capture is the default style: a single
|
||||
handler pattern captures the placeholder name (e.g. <message>) and fetches
|
||||
the example value from the scenario example store.
|
||||
|
||||
The handlers serve both specs/post-memo.feature and specs/memo-new.feature,
|
||||
whose wording differs but which share the same underlying Memo post behavior.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const MemoPost = require('../../src/services/memo-post')
|
||||
const NewPostPage = require('../../src/services/new-post')
|
||||
|
||||
const MEMO_POST_PREFIX = MemoPost.MEMO_POST_PREFIX
|
||||
|
||||
@@ -47,17 +52,27 @@ function createWorld () {
|
||||
const wallet = makeWallet('')
|
||||
const feed = makeFeed()
|
||||
const memoPost = new MemoPost({ wallet, feed })
|
||||
return {
|
||||
const world = {
|
||||
wallet,
|
||||
feed,
|
||||
memoPost,
|
||||
message: null,
|
||||
submitted: null
|
||||
currentPath: null,
|
||||
menuOpen: false
|
||||
}
|
||||
|
||||
// The New Post Page controller wraps the memo post behavior. Its navigate
|
||||
// adapter updates the world's current path so navigation can be asserted.
|
||||
world.newPage = new NewPostPage({
|
||||
memoPost,
|
||||
navigate: (path) => { world.currentPath = path },
|
||||
menuLinks: []
|
||||
})
|
||||
|
||||
return world
|
||||
}
|
||||
|
||||
// Handler registry. Each entry: { pattern, run }.
|
||||
// run receives (match, exampleStore, world).
|
||||
// run receives (match, exampleStore, world, step).
|
||||
const handlers = [
|
||||
{
|
||||
name: 'wallet authenticated for address',
|
||||
@@ -68,7 +83,7 @@ const handlers = [
|
||||
},
|
||||
{
|
||||
name: 'wallet has spendable output',
|
||||
pattern: /^the wallet has a spendable output to pay the transaction fee$/,
|
||||
pattern: /^the wallet has (?:a )?spendable output to pay the transaction fee$/,
|
||||
run (m, example, world) {
|
||||
world.wallet.utxos = [{ txid: 'utxo-for-fee', value: 100000 }]
|
||||
}
|
||||
@@ -76,34 +91,62 @@ const handlers = [
|
||||
{
|
||||
name: 'viewing recent posts feed',
|
||||
pattern: /^I am viewing the recent posts feed$/,
|
||||
run () {}
|
||||
run (m, example, world) {
|
||||
world.currentPath = NewPostPage.RECENT_FEED_PATH
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'compose memo text',
|
||||
pattern: /^I compose a memo with the text "<([A-Za-z0-9_]+)>"$/,
|
||||
name: 'navigate to path',
|
||||
pattern: /^I navigate to the path (.+)$/,
|
||||
run (m, example, world, step) {
|
||||
const target = m[1].trim()
|
||||
if (step.keyword === 'Then') {
|
||||
if (world.currentPath !== target) {
|
||||
throw new Error(`Expected to be on path ${target}, but current path is ${world.currentPath}.`)
|
||||
}
|
||||
} else {
|
||||
world.currentPath = target
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'open navigation menu',
|
||||
pattern: /^I open the navigation menu$/,
|
||||
run (m, example, world) {
|
||||
world.menuOpen = true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'menu shows link to path',
|
||||
pattern: /^the menu shows a link to the path (.+)$/,
|
||||
run (m, example, world) {
|
||||
const target = m[1].trim()
|
||||
if (!world.newPage.hasMenuLink(target)) {
|
||||
throw new Error(`Navigation menu does not link to ${target}.`)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'compose/type memo text',
|
||||
pattern: /^I (?:compose|type) a memo with the text "<([A-Za-z0-9_]+)>"$/,
|
||||
run (m, example, world) {
|
||||
const param = m[1]
|
||||
if (!(param in example)) {
|
||||
throw new Error(`Missing example value for "${param}"`)
|
||||
}
|
||||
world.message = example[param]
|
||||
world.newPage.setInput(example[param])
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'submit memo',
|
||||
pattern: /^I submit the memo$/,
|
||||
name: 'submit/click post',
|
||||
pattern: /^I (?:submit the memo|click the post button)$/,
|
||||
async run (m, example, world) {
|
||||
world.submitted = { error: null, txid: null }
|
||||
try {
|
||||
world.submitted.txid = await world.memoPost.post(world.message)
|
||||
} catch (err) {
|
||||
world.submitted.error = err
|
||||
}
|
||||
await world.newPage.submit()
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'broadcasts OP_RETURN with Memo post prefix',
|
||||
pattern: /^the wallet broadcasts an OP_RETURN transaction with the Memo post prefix$/,
|
||||
pattern: /^(?:the wallet|the app) broadcasts an OP_RETURN transaction with the Memo post prefix$/,
|
||||
run (m, example, world) {
|
||||
const broadcasts = world.wallet.broadcasts
|
||||
if (!broadcasts.length) {
|
||||
@@ -113,7 +156,7 @@ const handlers = [
|
||||
if (last.prefix !== MEMO_POST_PREFIX) {
|
||||
throw new Error(`Expected Memo post prefix ${MEMO_POST_PREFIX}, got "${last.prefix}".`)
|
||||
}
|
||||
if (last.msg !== world.message) {
|
||||
if (last.msg !== world.newPage.input) {
|
||||
throw new Error('Broadcast message text did not match the composed memo.')
|
||||
}
|
||||
}
|
||||
@@ -134,22 +177,34 @@ const handlers = [
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'app shows validation/length error',
|
||||
pattern: /^the app shows a (validation|length) error$/,
|
||||
name: 'page shows validation/length error',
|
||||
pattern: /^the (?:app|new post page) shows a (validation|length) error$/,
|
||||
run (m, example, world) {
|
||||
const kind = m[1]
|
||||
const expectedCode = kind === 'validation' ? 'memo_validation' : 'memo_length'
|
||||
if (!world.submitted || !world.submitted.error) {
|
||||
throw new Error(`Expected a ${kind} error but the submit succeeded.`)
|
||||
}
|
||||
if (world.submitted.error.code !== expectedCode) {
|
||||
throw new Error(`Expected ${expectedCode}, got ${world.submitted.error.code}.`)
|
||||
if (world.newPage.submitError !== expectedCode) {
|
||||
throw new Error(`Expected ${expectedCode}, got ${world.newPage.submitError}.`)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'wallet does not broadcast any transaction',
|
||||
pattern: /^the wallet does not broadcast any transaction$/,
|
||||
name: 'remaining character count',
|
||||
pattern: /^the new post page shows a remaining character count of <([A-Za-z0-9_]+)>$/,
|
||||
run (m, example, world) {
|
||||
const param = m[1]
|
||||
const expected = parseInt(example[param], 10)
|
||||
if (Number.isNaN(expected)) {
|
||||
throw new Error(`Invalid expected count for "${param}".`)
|
||||
}
|
||||
const actual = world.newPage.remainingCount()
|
||||
if (actual !== expected) {
|
||||
throw new Error(`Expected ${expected} remaining characters, got ${actual}.`)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'app does not broadcast any transaction',
|
||||
pattern: /^(?:the wallet|the app) does not broadcast any transaction$/,
|
||||
run (m, example, world) {
|
||||
if (world.wallet.broadcasts.length !== 0) {
|
||||
throw new Error('A transaction was broadcast when none was expected.')
|
||||
@@ -163,7 +218,7 @@ async function handleStep (step, example, world) {
|
||||
for (const handler of handlers) {
|
||||
const match = handler.pattern.exec(step.text)
|
||||
if (match) {
|
||||
await handler.run(match, example, world)
|
||||
await handler.run(match, example, world, step)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user